如何在SwiftUI环境中通过NSViewRepresentable自定义应用主菜单?
在SwiftUI的NSViewRepresentable中自定义应用主菜单的解决方案
SwiftUI的主菜单结构和纯AppKit环境不同——顶层菜单被苹果私有类SwiftUI.AppKitMainMenuItem包裹,直接操作NSApp.mainMenu的子项不会触发UI更新,因为SwiftUI维护着自己的菜单缓存层级。要在NSViewRepresentable里实现自定义主菜单,得先找到被包裹的真实NSMenu实例,再进行操作。
具体实现步骤
定位真实的主菜单实例
遍历NSApp.mainMenu的层级,通过类名特征识别私有菜单项,提取它持有的真实子菜单:private func findSwiftUIWrappedMainMenu() -> NSMenu? { guard let mainMenu = NSApp.mainMenu else { return nil } for item in mainMenu.items { // 通过类名特征匹配私有菜单项(兼容不同系统版本的类名变化) if String(describing: type(of: item)).contains("AppKitMainMenuItem") { return item.submenu } // 递归查找深层子菜单 if let submenu = item.submenu, let found = findSwiftUIWrappedMainMenu(in: submenu) { return found } } return nil } private func findSwiftUIWrappedMainMenu(in menu: NSMenu) -> NSMenu? { for item in menu.items { if String(describing: type(of: item)).contains("AppKitMainMenuItem") { return item.submenu } if let submenu = item.submenu, let found = findSwiftUIWrappedMainMenu(in: submenu) { return found } } return nil }添加自定义菜单项并触发更新
找到目标菜单后,添加菜单项,再强制刷新菜单Bar让修改生效:if let targetMenu = findSwiftUIWrappedMainMenu() { // 避免重复添加 guard !targetMenu.items.contains(where: { $0.title == "自定义操作" }) else { return } let customItem = NSMenuItem(title: "自定义操作", action: #selector(customAction), keyEquivalent: "") customItem.target = self // 绑定响应目标,比如Coordinator实例 targetMenu.addItem(customItem) // 触发菜单UI更新 NSApp.mainMenu?.update() NSApplication.shared.menuBarVisible = false NSApplication.shared.menuBarVisible = true }整合到NSViewRepresentable中
把逻辑放到Coordinator里,避免makeNSView重复执行导致的问题:import SwiftUI import AppKit struct CustomMenuEmbedView: NSViewRepresentable { func makeNSView(context: Context) -> NSView { let view = NSView() context.coordinator.setupCustomMenu() return view } func updateNSView(_ nsView: NSView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator() } class Coordinator: NSObject { func setupCustomMenu() { // 优先处理SwiftUI环境 if let targetMenu = findSwiftUIWrappedMainMenu() { addCustomItem(to: targetMenu) } else { // 兼容纯AppKit环境 guard let mainMenu = NSApp.mainMenu else { return } addCustomItem(to: mainMenu) } } private func addCustomItem(to menu: NSMenu) { guard !menu.items.contains(where: { $0.title == "自定义操作" }) else { return } let customItem = NSMenuItem(title: "自定义操作", action: #selector(onCustomAction), keyEquivalent: "") customItem.target = self menu.addItem(customItem) NSApp.mainMenu?.update() NSApplication.shared.menuBarVisible.toggle() } @objc private func onCustomAction() { print("自定义菜单被触发") } // 菜单查找逻辑 private func findSwiftUIWrappedMainMenu() -> NSMenu? { guard let mainMenu = NSApp.mainMenu else { return nil } for item in mainMenu.items { if String(describing: type(of: item)).contains("AppKitMainMenuItem") { return item.submenu } if let submenu = item.submenu, let found = findSwiftUIWrappedMainMenu(in: submenu) { return found } } return nil } private func findSwiftUIWrappedMainMenu(in menu: NSMenu) -> NSMenu? { for item in menu.items { if String(describing: type(of: item)).contains("AppKitMainMenuItem") { return item.submenu } if let submenu = item.submenu, let found = findSwiftUIWrappedMainMenu(in: submenu) { return found } } return nil } } }
注意事项
- 私有类名
AppKitMainMenuItem可能随macOS版本变化,代码里用包含判断而非精确匹配,提升兼容性 - 开发通用扩展时,可通过是否能找到私有菜单项来区分SwiftUI/纯AppKit环境,分别处理
- 菜单响应的target要绑定到生命周期稳定的对象(比如Coordinator),避免SwiftUI视图销毁导致响应失效
内容的提问来源于stack exchange,提问作者Vitalii Vashchenko
相关产品推荐
相关产品推荐

