ios - 如何在 SwiftUI View 中访问自己的窗口?

标签 ios swift macos swiftui

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

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

像这样的东西

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

或通过在每个 SwiftUI View 中添加 UIViewRepresentable/NSViewRepresentable 来使用 view.window 获取窗口看起来丑陋,沉重,而且不可用。

因此,我该怎么做?

最佳答案

SwiftUI Lift-Cycle (SwiftUI 2+)
这是一个解决方案(使用 Xcode 13.4 测试),仅适用于 iOS

  • 我们需要应用程序委托(delegate)来使用我们的场景委托(delegate)类
  • 创建场景配置
    @main
    struct PlayOn_iOSApp: App {
        @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        // ...
    }
    
    class AppDelegate: NSObject, UIApplicationDelegate {
    
    
        func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
            let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
            if connectingSceneSession.role == .windowApplication {
                configuration.delegateClass = SceneDelegate.self
            }
            return configuration
        }
    }
    
  • 声明我们的SceneDelegate并向两者确认 (!!!+) UIWindowSceneDelegateObservableObject
  • class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
        var window: UIWindow?   // << contract of `UIWindowSceneDelegate`
    
        func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
            guard let windowScene = scene as? UIWindowScene else { return }
            self.window = windowScene.keyWindow   // << store !!!
        }
    }
    
    
  • 现在我们可以在 View 层次结构中的任何地方(!!!)使用我们的委托(delegate)作为 EnvironmentObject ,因为(确认到 ObservableObject 的奖励)SwiftUI 自动将其注入(inject) ContentView
  •     @EnvironmentObject var sceneDelegate: SceneDelegate
        
        var body: some View {
             // ...       
                .onAppear {
                    if let myWindow = sceneDelegate.window {
                        print(">> window: \(myWindow.description)")
                    }
                }
        }
    
    demo3
    Complete code in project is here
    UIKit 生命周期
    这是我的实验结果,看起来很适合我,因此可能也会有帮助。使用 Xcode 11.2/iOS 13.2/macOS 15.0 测试
    这个想法是使用原生的 SwiftUI 环境概念,因为一旦注入(inject)的环境值就可以自动用于整个 View 层次结构。所以
  • 定义环境键。注意,需要记住避免在保留窗口
  • 上进行引用循环。
    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
            }
        }
    }
    
  • 在根 ContentView 中注入(inject)宿主窗口来代替窗口创建(在 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
    
  • 仅在需要时使用,只需声明环境变量
  • 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)
                }
            }
        }
    }
    
    backup

    关于ios - 如何在 SwiftUI View 中访问自己的窗口?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60359808/

    相关文章:

    ios - UNCalendarNotificationTrigger与开始日期

    swift - 如何理解这个rx-swift的map函数

    objective-c - Mac 唤醒时如何调用方法

    objective-c - 为什么我的 NSPanel 没有正常的灰色背景?

    ios - 我如何将一个 PickerView 用于显示不同数据的几个按钮

    ios - 具有状态和绑定(bind)的 ForEach NavigationLink

    ios - 在编辑表格 View 单元格时获取滑动事件

    swift - 在 Swift 中将类作为值存储在字典中的语法是什么?

    ios - 表格单元格未加载到滚动条上

    iphone - 在表格 View 单元格中动态更改按钮