You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 07:18:19