如何在基于NSApplicationDelegate的AppKit应用中使用MenuBarExtra Scene
传统AppKit应用(以@main标记的NSApplicationDelegate为入口)没法直接使用SwiftUI的MenuBarExtra Scene,因为后者是基于SwiftUI的App生命周期(App协议)设计的。不过可以通过NSStatusItem + NSHostingController的组合实现完全一致的效果,具体步骤如下:
1. 定义SwiftUI状态栏内容视图
先编写你需要的自定义UI,比如带交互元素的菜单视图:
import SwiftUI struct StatusBarMenuView: View { @Binding var isMenuOpen: Bool var body: some View { VStack(spacing: 12) { Text("自定义状态栏菜单") .font(.headline) Button(action: { print("执行操作") isMenuOpen = false // 操作完成后关闭菜单 }) { Text("点击执行") .padding(.horizontal, 20) .padding(.vertical, 8) .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } Button(action: { isMenuOpen = false }) { Text("关闭") .foregroundColor(.red) } } .padding(20) .frame(width: 250) } }
2. 在AppDelegate中初始化状态栏项与托管控制器
在你的NSApplicationDelegate子类里,添加状态栏项和SwiftUI视图托管控制器的属性,并在应用启动时完成初始化:
import Cocoa import SwiftUI @main class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! // 状态栏项实例 private var statusItem: NSStatusItem! // 托管SwiftUI视图的控制器 private var hostingController: NSHostingController<StatusBarMenuView>! // 控制菜单展开/收起的状态变量 private var isMenuOpen = false func applicationDidFinishLaunching(_ aNotification: Notification) { // 创建状态栏项 statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) // 设置状态栏图标(可替换为自定义图标) if let button = statusItem.button { button.image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "设置") button.action = #selector(toggleMenu(_:)) } // 初始化SwiftUI视图的托管控制器 let menuView = StatusBarMenuView(isMenuOpen: $isMenuOpen) hostingController = NSHostingController(rootView: menuView) // 配置弹出窗口样式,模拟MenuBarExtra的非激活面板特性 hostingController.view.window?.styleMask = [.borderless, .nonactivatingPanel] hostingController.view.window?.level = .statusBar hostingController.view.window?.backgroundColor = .clear } // 切换菜单显示/隐藏的方法 @objc private func toggleMenu(_ sender: Any?) { isMenuOpen.toggle() if isMenuOpen { guard let button = statusItem.button, let window = hostingController.view.window else { return } // 计算窗口位置,对齐状态栏按钮 let buttonFrame = button.convert(button.bounds, to: nil) let screenFrame = NSScreen.main?.frame ?? NSZeroRect let windowOrigin = NSPoint( x: buttonFrame.origin.x + (buttonFrame.width - window.frame.width)/2, y: screenFrame.height - buttonFrame.origin.y - buttonFrame.height - window.frame.height ) window.setFrameOrigin(windowOrigin) window.makeKeyAndOrderFront(nil) } else { hostingController.view.window?.orderOut(nil) } } }
3. 添加外部点击关闭菜单的逻辑
为了让用户点击菜单外部时自动关闭,需要在applicationDidFinishLaunching中添加全局事件监听:
func applicationDidFinishLaunching(_ aNotification: Notification) { // ... 保留之前的初始化代码 ... // 监听全局鼠标点击事件,点击菜单外部时关闭 NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] event in guard let self = self, self.isMenuOpen else { return } let window = self.hostingController.view.window! let eventLocation = window.convert(event.locationInWindow, from: nil) if !window.frame.contains(eventLocation) { self.isMenuOpen = false window.orderOut(nil) } } }
关键说明
- 这种实现完全模拟了
MenuBarExtra的核心行为:点击状态栏图标切换菜单显示、点击外部自动关闭,同时支持任意SwiftUI自定义UI,比传统NSMenuItem菜单更灵活。 - 如果只需要简单的下拉列表菜单,可以直接给
NSStatusItem设置传统NSMenu;若要丰富UI,则采用上述方法。
内容的提问来源于stack exchange,提问作者NightFuryLxD
相关产品推荐
相关产品推荐

