【问题标题】:How to access own window within SwiftUI view?如何在 SwiftUI 视图中访问自己的窗口?
【发布时间】:2020-06-07 03:10:29
【问题描述】:

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

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

类似的东西

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

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

那么,我该怎么做呢?

【问题讨论】:

    标签: ios swift macos swiftui


    【解决方案1】:

    这是我的实验结果,看起来很适合我,所以大家可能会发现它也很有帮助。使用 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
            }
        }
    }
    

    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
    

    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)
                }
            }
        }
    }
    

    【讨论】:

    • 如果你使用新的@main“only SwiftUI”东西,你知道如何做到这一点吗?
    • 您必须使用带有(空的)UIViewUIViewRepresentable,在附加/分离时抓取并报告其 window
    • @NicolaiHenriksen 您找到解决方案了吗?我也被困在完全相同的事情上。
    • @aheze,这是可能的解决方案stackoverflow.com/a/63276688/12299030
    【解决方案2】:

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

    final class AppData: ObservableObject {
        let window: UIWindow? // Will be nil in SwiftUI previewers
    
        init(window: UIWindow? = nil) {
            self.window = window
        }
    }
    

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

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

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

    struct MyView: View {
        @EnvironmentObject private var appData: AppData
        // Use appData.window in your view's body.
    }
    

    【讨论】:

    • 不错的解决方案。尚未测试保留周期,但我个人更喜欢使用 weak var window 代替 let window 并且到目前为止运行良好。
    【解决方案3】:

    起初我喜欢@Asperi 给出的答案,但是在我自己的环境中尝试它时,我发现很难开始工作,因为我需要在创建窗口时知道根视图(因此我不知道)在我创建根视图时不知道窗口)。所以我按照他的例子,但我选择使用环境对象而不是环境值。这具有几乎相同的效果,但对我来说更容易开始工作。以下是我使用的代码。请注意,我创建了一个泛型类,它在给定 SwiftUI 视图的情况下创建一个 NSWindowController。 (请注意,userDefaultsManager 是我在应用程序的大多数窗口中需要的另一个对象。但我认为,如果您删除该行加上 appDelegate 行,您最终会得到一个几乎可以在任何地方工作的解决方案。 )

    class RootViewWindowController<RootView : View>: NSWindowController {
        convenience init(_ title: String,
                         withView rootView: RootView,
                         andInitialSize initialSize: NSSize = NSSize(width: 400, height: 500))
        {
            let appDelegate: AppDelegate = NSApplication.shared.delegate as! AppDelegate
            let windowWrapper = NSWindowWrapper()
            let actualRootView = rootView
                .frame(width: initialSize.width, height: initialSize.height)
                .environmentObject(appDelegate.userDefaultsManager)
                .environmentObject(windowWrapper)
            let hostingController = NSHostingController(rootView: actualRootView)
            let window = NSWindow(contentViewController: hostingController)
            window.setContentSize(initialSize)
            window.title = title
            windowWrapper.rootWindow = window
            self.init(window: window)
        }
    }
    
    final class NSWindowWrapper: ObservableObject {
        @Published var rootWindow: NSWindow? = nil
    }
    

    然后在我认为需要它的地方(以便在适当的时间关闭窗口),我的结构开始如下:

    struct SubscribeToProFeaturesView: View {
        @State var showingEnlargedImage = false
        @EnvironmentObject var rootWindowWrapper: NSWindowWrapper
    
        var body: some View {
            VStack {
                Text("Professional Version Upgrade")
                    .font(.headline)
                VStack(alignment: .leading) {
    

    在我需要关闭窗口的按钮中

    self.rootWindowWrapper.rootWindow?.close()
    

    它不像我希望的那样干净(我希望有一个解决方案,我只是说self.rootWindow?.close() 而不是需要包装类),但这还不错,它允许我在创建窗口之前创建 rootView 对象。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-12
      • 2017-10-07
      • 1970-01-01
      相关资源
      最近更新 更多