如何在 SwiftUI 视图中访问自己的窗口?

Asp*_*eri 8 macos ios swift swiftui

目标是在 SwiftUI 视图层次结构的任何级别轻松访问托管窗口。目的可能不同——关闭窗口、退出第一响应者、替换根视图或 contentViewController。与 UIKit/AppKit 的集成有时也需要通过窗口路径,所以……

我在这里遇到并尝试过的东西,

像这样的东西

let keyWindow = shared.connectedScenes
        .filter({$0.activationState == .foregroundActive})
        .map({$0 as? UIWindowScene})
        .compactMap({$0})
        .first?.windows
        .filter({$0.isKeyWindow}).first
Run Code Online (Sandbox Code Playgroud)

或者通过在每个 SwiftUI 视图中添加 UIViewRepresentable/NSViewRepresentable 来使用view.window看起来丑陋、沉重且不可用的窗口。

因此,我将如何做到这一点?

Asp*_*eri 15

这是我的实验结果,看起来很适合我,所以人们可能会发现它也有帮助。使用 Xcode 11.2 / iOS 13.2 / macOS 15.0 测试

这个想法是使用原生的 SwiftUI 环境概念,因为一旦注入的环境值自动可用于整个视图层次结构。所以

1) 定义环境键。注意,需要记住避免在保持窗口上进行引用循环

struct HostingWindowKey: EnvironmentKey {

#if canImport(UIKit)
    typealias WrappedValue = UIWindow
#elseif canImport(AppKit)
    typealias WrappedValue = NSWindow
#else
    #error("Unsupported platform")
#endif

    typealias Value = () -> WrappedValue? // needed for weak link
    static let defaultValue: Self.Value = { nil }
}

extension EnvironmentValues {
    var hostingWindow: HostingWindowKey.Value {
        get {
            return self[HostingWindowKey.self]
        }
        set {
            self[HostingWindowKey.self] = newValue
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

2) 在根 ContentView 中注入托管窗口以代替窗口创建(在 AppDelegate 或 SceneDelegate 中,只需一次

// window created here

let contentView = ContentView()
                     .environment(\.hostingWindow, { [weak window] in
                          return window })

#if canImport(UIKit)
        window.rootViewController = UIHostingController(rootView: contentView)
#elseif canImport(AppKit)
        window.contentView = NSHostingView(rootView: contentView)
#else
    #error("Unsupported platform")
#endif
Run Code Online (Sandbox Code Playgroud)

3)只在需要的地方使用,只需声明环境变量

struct ContentView: View {
    @Environment(\.hostingWindow) var hostingWindow

    var body: some View {
        VStack {
            Button("Action") {
                // self.hostingWindow()?.close() // macOS
                // self.hostingWindow()?.makeFirstResponder(nil) // macOS
                // self.hostingWindow()?.resignFirstResponder() // iOS
                // self.hostingWindow()?.rootViewController?.present(UIKitController(), animating: true)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 如果您使用新的“@main”“仅 SwiftUI”内容,您知道如何执行此操作吗? (4认同)
  • @hnh,/sf/answers/4429368191/ (2认同)

Tot*_*nai 7

通过接收访问当前窗口NSWindow.didBecomeKeyNotification

.onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)) { notification in
    if let window = notification.object as? NSWindow {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)


Edw*_*rey 5

将窗口添加为环境对象中的属性。这可以是您用于其他应用程序范围数据的现有对象。

final class AppData: ObservableObject {
    let window: UIWindow? // Will be nil in SwiftUI previewers

    init(window: UIWindow? = nil) {
        self.window = window
    }
}
Run Code Online (Sandbox Code Playgroud)

创建环境对象时设置该属性。将对象添加到视图层次结构基础的视图中,例如根视图。

let window = UIWindow(windowScene: windowScene) // Or however you initially get the window
let rootView = RootView().environmentObject(AppData(window: window))
Run Code Online (Sandbox Code Playgroud)

最后,在您的视图中使用窗口。

struct MyView: View {
    @EnvironmentObject private var appData: AppData
    // Use appData.window in your view's body.
}
Run Code Online (Sandbox Code Playgroud)