Search code examples
swiftmacosswiftuigoogle-oauthappkit

How could I pass StateObject to both main struct and AppDelegate(or just AppDelegate)?


I began to learn Swift four days ago and I met a problem.

@main
struct GmailNotificationApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    @StateObject var authViewModel = AuthenticationViewModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(authViewModel)           // <-----first place
        }
    }
}

class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
    private var statusItem: NSStatusItem!
    private var popover: NSPopover!
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        
        if let statusButton = statusItem.button {
            statusButton.image = NSImage(systemSymbolName: "g.circle.fill", accessibilityDescription: "Gmail Notification")
            statusButton.action = #selector(togglePopover)
        }
        
        self.popover = NSPopover()
        self.popover.contentSize = NSSize(width: 210, height: 300)
        self.popover.behavior = .transient
        self.popover.contentViewController = NSHostingController(rootView: ContentView().environmentObject(AuthenticationViewModel()))    // <-----second place
    }
    
    @objc func togglePopover(){
        if let button = statusItem.button {
            if popover.isShown {
                self.popover.performClose(nil)
            } else {
                self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
            }
        }
    }
}

My question is: How could I pass StateObject inside AppDelegate?

Also:

I knew little about AppKit, it seems I have two "window", one is displayed just below menu bar and another shows when Google Auth calls back.

I wish the windows in the center of my screen never shows. Now I use

NSApplication.shared.windows.first?.close()
                    
var window: NSWindow!
window = NSWindow(
    contentRect: NSRect(x: 0, y: 0, width: 0, height: 0),
    styleMask: [.borderless],
    backing: .buffered, defer: false)
window.center()
window.makeKeyAndOrderFront(nil)

to work around. But I think it's very stupid. So is there a way to never show the window in the screen center and just show the windows below menu bar? I'm quite new to Swift/SwiftUI/AppKit and sorry for these dumb questions.


Solution

  • Notice that you don't give SwiftUI an instance of your AppDelegate. You only give it the class object, AppDelegate.self, and SwiftUI creates the instance once and keeps it around forever.

    I think a better pattern is to create the AuthenticationViewModel in your AppDelegate. Then you don't need to use @StateObject to keep the model alive, because the app delegate will do that. So you can just use @ObservedObject:

    class AppDelegate: NSObject, NSApplicationDelegate {
        let authViewModel = AuthenticationViewModel()
    
        // blah blah blah
    }
    
    @main
    struct MacStudyApp: App {
        @NSApplicationDelegateAdaptor private var appDelegate: AppDelegate
        @ObservedObject private var authViewModel: AuthenticationViewModel
    
        init() {
            authViewModel = _appDelegate.wrappedValue.authViewModel
        }
    
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environmentObject(authViewModel)
            }
        }
    }
    

    But in your case, you don't even need to use @ObservedObject because MacStudyApp doesn't use any properties of authViewModel in its body. So you could in fact just do this:

    class AppDelegate: NSObject, NSApplicationDelegate {
        let authViewModel = AuthenticationViewModel()
    
        // blah blah blah
    }
    
    @main
    struct MacStudyApp: App {
        @NSApplicationDelegateAdaptor private var appDelegate: AppDelegate
    
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environmentObject(appDelegate.authViewModel)
            }
        }
    }