visionOS 中如何实现重启应用时自动恢复窗口位置?
实现SwiftUI多窗口位置的持久化恢复
对于用@Environment(\.openWindow)创建的多窗口,要实现重启后恢复窗口位置,可以通过状态持久化存储结合窗口生命周期监听来实现,以下是具体方案:
1. 定义窗口状态模型与管理器
首先创建可序列化的窗口状态结构体,以及全局的状态管理器来加载、保存所有窗口的位置信息:
import SwiftUI // 存储单个窗口的ID和位置大小 struct WindowState: Codable, Identifiable { let id: String var frame: CGRect } // 管理所有窗口状态的全局管理器 class WindowStateManager: ObservableObject { @Published var windowStates: [WindowState] = [] // 从UserDefaults加载已保存的状态 func loadStates() { guard let data = UserDefaults.standard.data(forKey: "appWindowStates") else { return } windowStates = (try? JSONDecoder().decode([WindowState].self, from: data)) ?? [] } // 将当前状态保存到UserDefaults func saveStates() { guard let data = try? JSONEncoder().encode(windowStates) else { return } UserDefaults.standard.set(data, forKey: "appWindowStates") } }
2. 在App中注入管理器并关联窗口ID
在你的App主结构体中,初始化状态管理器,并为每个通过openWindow打开的窗口分配唯一ID,同时监听窗口的出现/消失事件来应用或保存状态:
@main struct MultiWindowDemoApp: App { @StateObject private var stateManager = WindowStateManager() var body: some Scene { // 基于字符串ID的WindowGroup,每个窗口对应唯一ID WindowGroup(for: String.self) { $windowID in WindowContentView(windowID: windowID) .environmentObject(stateManager) .onAppear { // 窗口出现时,恢复已保存的位置 applySavedState(for: windowID) } .onDisappear { // 窗口消失时,保存当前位置 saveCurrentState(for: windowID) } } .commands { CommandGroup(after: .newItem) { Button("打开窗口A") { openWindow(id: "window_A") } Button("打开窗口B") { openWindow(id: "window_B") } } } .onAppear { // 应用启动时加载所有保存的窗口状态 stateManager.loadStates() } } // 恢复指定ID窗口的位置 private func applySavedState(for windowID: String) { guard let state = stateManager.windowStates.first(where: { $0.id == windowID }), let window = NSApplication.shared.windows.first(where: { $0.identifier?.rawValue == windowID }) else { return } window.setFrame(state.frame, display: true) } // 保存指定ID窗口的当前位置 private func saveCurrentState(for windowID: String) { guard let window = NSApplication.shared.windows.first(where: { $0.identifier?.rawValue == windowID }) else { return } let currentFrame = window.frame if let index = stateManager.windowStates.firstIndex(where: { $0.id == windowID }) { stateManager.windowStates[index].frame = currentFrame } else { stateManager.windowStates.append(WindowState(id: windowID, frame: currentFrame)) } stateManager.saveStates() } }
3. 实时监听窗口位置变化(可选)
如果希望用户拖动窗口时实时保存位置,而不是等到窗口消失,可以通过NSWindowDelegate监听窗口的移动/ resize事件:
struct WindowContentView: View { let windowID: String @EnvironmentObject private var stateManager: WindowStateManager var body: some View { VStack { Text("窗口 \(windowID)") .font(.title) } .frame(width: 300, height: 200) .onAppear { // 为当前窗口设置代理 guard let window = NSApplication.shared.windows.first(where: { $0.identifier?.rawValue == windowID }) else { return } window.delegate = WindowStateDelegate(windowID: windowID, manager: stateManager) // 确保窗口有唯一标识符 window.identifier = NSUserInterfaceItemIdentifier(rawValue: windowID) } } } // 窗口代理,监听位置和大小变化 class WindowStateDelegate: NSObject, NSWindowDelegate { let windowID: String let manager: WindowStateManager init(windowID: String, manager: WindowStateManager) { self.windowID = windowID self.manager = manager super.init() } func windowDidMove(_ notification: Notification) { updateWindowState(from: notification) } func windowDidResize(_ notification: Notification) { updateWindowState(from: notification) } private func updateWindowState(from notification: Notification) { guard let window = notification.object as? NSWindow else { return } let currentFrame = window.frame if let index = manager.windowStates.firstIndex(where: { $0.id == windowID }) { manager.windowStates[index].frame = currentFrame } else { manager.windowStates.append(WindowState(id: windowID, frame: currentFrame)) } manager.saveStates() } }
关键注意事项
- 存储方式:示例中用
UserDefaults存储,适合轻量数据;如果窗口数量多或需要更复杂的状态,可以改用Core Data或本地文件存储。 - 窗口ID唯一性:必须确保每个窗口的ID唯一,否则会导致状态覆盖或恢复错误。
- 关闭窗口的处理:如果希望用户关闭窗口后下次启动不再恢复,可以在
windowWillClose代理方法中移除对应窗口的状态。 - 跨平台适配:如果是iOS/iPadOS的多Scene窗口,原理类似,只需将
NSWindow替换为UIWindowScene,监听对应的生命周期事件即可。
内容的提问来源于stack exchange,提问作者Ben Gottlieb
相关产品推荐
相关产品推荐

