macOS SwiftUI:在Window菜单添加带快捷键的可切换菜单项
在macOS SwiftUI中实现带勾选状态的Window菜单开关项
需求与问题
需要在macOS SwiftUI应用的Window菜单中添加一个类似Xcode「Window」→「Developers Documentation」的菜单项,要求:
- 菜单项带有快捷键
- 窗口打开时显示勾选标记
- 点击菜单项可切换窗口的开闭状态
尝试过Toggle、Button组件,遇到的问题:
- Button仅能单次打开窗口,关闭后无法再次触发打开
- Toggle仅能完成一次开闭操作,后续失效
- 使用StateObject后代码可运行,但日志出现
onChange(of: Bool) action tried to update multiple times per frame错误,快速按快捷键会生成重复窗口
解决方案
核心思路是让菜单项的勾选状态与窗口的实际生命周期同步,利用SwiftUI环境变量和窗口事件监听控制状态,避免重复触发和重复窗口。
完整代码示例
import SwiftUI // 全局状态管理,同步窗口打开状态 class MenuState: ObservableObject { @Published var isProductWindowOpen = false // 防止重复触发的标记 private var isProcessing = false func toggleProductWindow() { guard !isProcessing else { return } isProcessing = true if isProductWindowOpen { // 关闭指定标题的窗口 NSApplication.shared.windows.forEach { window in if window.title == "Products" { window.close() } } } else { // 触发系统打开窗口的动作 NSApplication.shared.sendAction(Selector(("showWindow:")), to: nil, from: nil) } // 延迟重置标记,避免同一帧内多次触发 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.isProcessing = false } } // 从窗口生命周期事件更新状态 func updateWindowState(isOpen: Bool) { if isProductWindowOpen != isOpen { isProductWindowOpen = isOpen } } } @main struct MenuItemInsertWindowApp: App { @StateObject private var menuState = MenuState() // 监听应用窗口事件的代理 @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate var body: some Scene { WindowGroup { ContentView() } WindowGroup("Products") { ProductsView() .onAppear { menuState.updateWindowState(isOpen: true) } .onDisappear { menuState.updateWindowState(isOpen: false) } } .commands { // 替换默认的Window列表组,插入自定义菜单项 CommandGroup(replacing: .windowList) { Toggle("Products", isOn: $menuState.isProductWindowOpen) .onChange(of: menuState.isProductWindowOpen) { _ in menuState.toggleProductWindow() } .keyboardShortcut("P", modifiers: [.command, .shift]) // 设置快捷键 } } } // 应用代理,处理手动关闭窗口的情况 class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } func applicationWillClose(_ notification: Notification) { guard let window = notification.object as? NSWindow, window.title == "Products" else { return } menuState.updateWindowState(isOpen: false) } } } struct ContentView: View { var body: some View { Text("ContentView") .frame(minWidth: 600, maxWidth: 600, minHeight: 400, maxHeight: 400) } } struct ProductsView: View { var body: some View { Text("Products Window") .frame(minWidth: 500, maxWidth: 500, minHeight: 300, maxHeight: 300) } }
关键部分说明
MenuState状态管理:
专门维护窗口的打开状态,加入isProcessing开关防止短时间内重复操作(比如快速按快捷键),彻底解决重复窗口和日志报错问题。updateWindowState方法用来同步窗口实际的显示/隐藏状态,保证菜单项勾选标记和窗口状态一致。窗口生命周期同步:
在ProductsView的onAppear和onDisappear中更新状态,用户手动关闭窗口时,通过AppDelegate的applicationWillClose事件同步状态,不会出现窗口已关闭但菜单项仍勾选的情况。菜单实现:
用Toggle组件实现自带勾选效果的菜单项,绑定状态后,点击或按快捷键都会触发toggleProductWindow控制窗口开闭,同时状态自动同步更新。
内容的提问来源于stack exchange,提问作者iPadawan
相关产品推荐
相关产品推荐

