Amadeu Cavalcante Filho
Amadeu Cavalcante Filho

Reputation: 2398

How to access NSWindow from @main App using only SwiftUI?

At this answer the solution work for Scene plus swiftUI.

However using @main like:

@main
struct MyApp: App {
    @StateObject private var model = MyModel()
    
    var body: some Scene {
        WindowGroup {
            Router {
                AppContent()
            }.environmentObject(self.model)
        }
    }
}

I also tried to get the main window by using

var window: NSWindow? {
        let window = NSApplication.shared.mainWindow
        return window
    }

Nevertheless, the mainWindow always return nil

Update:

I need the NSWindow due to the need of conforming with ASWebAuthenticationPresentationContextProviding which obligates to return a NSWindow. Basically, I'm trying to do something like:

LoginView(store: AuthStore(window: window))

Where AuthStore uses the AuthenticationServices to perform an authentication.

Upvotes: 16

Views: 7774

Answers (3)

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 120022

You can access the keyWindow through an @Environment variable from anywhere like:

@Environment(\.keyWindow) private var keyWindow

By simply extending the EnvironmentValues it like:

extension EnvironmentValues {
    var keyWindow: NSWindow? {
        get { self[KeyWindow.self] }
        set { self[KeyWindow.self] = newValue }
    }
}

struct KeyWindow: EnvironmentKey {
    static var defaultValue = NSApplication.shared.keyWindow
}

💡 Keep it updated

You need to add a simple logic to keep the keyWindow always up-to-date like:

@main
struct MyApp: App {
    @State private var keyWindow = KeyWindow.defaultValue // 👈 A simple storage

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)) { keyWindow = $0.object as? NSWindow }  // 👈 Update the storage on change
                .environment(\.keyWindow, keyWindow) // 👈 Pass in the updated window
        }
    }
} 

Here is the UIKit version

Upvotes: 2

Asperi
Asperi

Reputation: 258443

Basically, I'm trying to do something like:

LoginView(store: AuthStore(window: window))

Here is a demo of possible approach (with some replicated entities)

demo

class AuthStore {
    var window: NSWindow

    init(window: NSWindow) {
        self.window = window
    }
}

struct DemoWindowAccessor: View {
    @State private var window: NSWindow?   // << detected in run-time so optional
    var body: some View {
        VStack {
            if nil != window {
                LoginView(store: AuthStore(window: window!))    // << usage
            }
        }.background(WindowAccessor(window: $window))
    }
}

struct WindowAccessor: NSViewRepresentable {
    @Binding var window: NSWindow?

    func makeNSView(context: Context) -> NSView {
        let view = NSView()
        DispatchQueue.main.async {
            self.window = view.window   // << right after inserted in window
        }
        return view
    }

    func updateNSView(_ nsView: NSView, context: Context) {}
}

struct LoginView: View {
    let store: AuthStore

    var body: some View {
        Text("LoginView with Window: \(store.window)")
    }
}

Upvotes: 24

arthas
arthas

Reputation: 818

Many ways but there is a gotcha:

NSApplication.shared.keyWindow, NSApp.keyWindow, 
NSApp.mainWindow, 

Sometimes, these can return nil especially during launch or if the app is inactive.

I think it is because setting these properties may not be instantaneous from the Appkit side of things

The best way is to access all windows:

NSApp.windows.first

This will return all windows but in an unpredictable order.

If you have multiple windows you can do further filtering to find the required window but this method avoids that crazy behaviour where the other methods return nil.

Upvotes: 3

Related Questions