Xcode 12.4 SwiftUI macOS 二级窗口如何创建并共享主窗口的Environment Object?
问题场景梳理
- 开发环境:Xcode 12.4 下开发 macOS 平台 SwiftUI 应用
- 当前已实现的开窗逻辑:通过
NSApp.sendAction(#selector(AppDelegate.openSecondaryWindow), to: nil, from: nil)触发二级窗口创建流程,采用手动实例化NSWindow+NSHostingView承载SwiftUI视图的方式实现 - 待解决的核心问题:
- 咨询当前手动通过AppDelegate管理NSWindow的开窗方案是否存在更优替代
- 自定义二级视图
SecondaryView无法获取主窗口ContentView使用的同一个globalState环境对象
- 附原始实现代码:
class AppDelegate: NSObject, NSApplicationDelegate { var secondaryWindow: NSWindow! func applicationDidFinishLaunching(_ notification: Notification) { [...] } func applicationWillFinishLaunching(_ notification: Notification) { NSWindow.allowsAutomaticWindowTabbing = false } func applicationShouldTerminateAfterLastWindowClosed(_ app: NSApplication) -> Bool { return true } func applicationWillTerminate(_ notification: Notification) { [...] } @objc func openSecondaryWindow () { if self.secondaryWindow == nil { let secondaryView = SecondaryView() self.secondaryWindow = NSWindow( contentRect: NSRect(x: 20, y: 20, width: 480, height: 300), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false) // 注意:原代码此处为笔误,实际指向未定义的spreadWindow变量 self.spreadWindow.contentView = NSHostingView(rootView: secondaryView) } self.spreadWindow.makeKeyAndOrderFront(nil) } } @main struct MyApp: App { @StateObject var globalState: GlobalState = GlobalState.shared @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() .environmentObject(self.globalState) } } }
问题解决方案
环境对象共享的最小改动修复
当前SecondaryView拿不到globalState的核心原因,是手动创建NSHostingView时没有给根视图挂载环境对象。
结合现有globalState基于单例GlobalState.shared初始化的逻辑,最快的修复方式是在创建SecondaryView时直接挂载对应单例:
@objc func openSecondaryWindow () { if self.secondaryWindow == nil { // 手动为二级视图挂载和主窗口一致的环境对象 let secondaryView = SecondaryView() .environmentObject(GlobalState.shared) self.secondaryWindow = NSWindow( contentRect: NSRect(x: 20, y: 20, width: 480, height: 300), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false) self.secondaryWindow.contentView = NSHostingView(rootView: secondaryView) } self.secondaryWindow.makeKeyAndOrderFront(nil) }
这种改法和主窗口的globalState引用完全一致,不存在状态不同步的问题,改动量最小。
如果不想依赖单例做跨层传递,可以通过@NSApplicationDelegateAdaptor建立的实例关联,在AppDelegate中新增var sharedState: GlobalState?属性,App初始化完成后把当前实例的globalState赋值给该属性,创建二级视图时直接从该属性读取挂载即可。
手动管理NSWindow方式的优化替代
当前通过AppDelegate手动实例化NSWindow的方案虽然可运行,但存在两个固有缺陷:
- 窗口生命周期脱离SwiftUI的Scene体系管理,无法跟随系统的窗口状态恢复逻辑自动重建,还容易因为手动持有引用导致窗口意外释放
- 每经过一层AppKit桥接,就需要手动处理一次环境对象的依赖注入,容易出现遗漏挂载的问题
适配Xcode 12.4版本的更优替代方案: - 如果不需要极端自定义窗口的层级或外观逻辑,直接在App的Scene定义中新增原生
Window场景承载SecondaryView,环境对象可以直接通过场景链路自动传递,不需要手动创建NSHostingView和NSWindow,打开窗口时通过系统API定位对应窗口展示即可。 - 如果必须要自定义窗口的样式配置(比如初始位置、fullSizeContentView这类参数),可以将窗口初始化逻辑迁移到SwiftUI的自定义
WindowStyle中实现,既保留自定义能力,又能让环境对象跟随场景链路自动传递,减少手动桥接的成本。
代码中需要修正的笔误:原
openSecondaryWindow方法中对spreadWindow的调用属于变量名拼写错误,需要统一替换为已声明的secondaryWindow,否则运行时会直接触发崩溃。
内容的提问来源于stack exchange,提问作者pistacchio
相关产品推荐
相关产品推荐

