
通过 SwiftUI 的 MenuBarEtra 实现菜单栏小工具
在 macOS 下有许多优秀的状态栏小工具,如果你想开发自己的状态栏小工具,看我这篇文章应该会对你有所帮助!很早之前写过一篇使用 AppKit 实现的文章,而今天,我们完全通过 SwiftUI 的 MenuBarExtra 快速实现,适用于 macOS 13+ 版本!
若使用 Appkit 的实现方式请参考:macOS 开发之菜单栏形式的状态栏小工具。
实现纯状态栏小工具,包含以下操作:
- 添加 MenuBarExtra 实现状态栏小工具;
- 删除主窗口;
- 实现 Dock 栏上不显示应用图标。
如果只是在已开发应用增加状态栏的操作入口,可忽略第二、三步!
平台
- macOS 13.5.2
- Xcode 15.0
- swift 5.9
本文使用上述平台实现,版本不同可能有些差异,但基本思路一致。
新建工程
我们先新建一个工程,当然也可以在自己已有工程中添加实现,注意要将应用的 Deplyment Target 设置到 13.0 以上。
New Project -> macOS -> App,点击 Next,我这里 Product Name 设置为 MenuBarAppDemo,Language 选择 Swift,Interface 选择 SwiftUI,最后选择保存目录点击创建 Create 。
记得要配置一下 Project 和 Target 的 Deployment Target 版本。
MenuBarExtra 支持 macOS 13.0+,我这里就选 13.0 了。
添加 MenuBarExtra
打开 App 文件 MenuBarAppDemoApp.swift,在 WindowsGroup 下面添加合适的 MenuBarExtra 即可。
有八种方式,按照自己的需求选择就好,我这里选择使用 SF Symbol 图像作为图标的接口 MenuBarExtra(content:label:)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21import SwiftUI @main struct MenuBarAppDemoApp: App { var body: some Scene { WindowGroup { ContentView() } MenuBarExtra(content: { ContentView() }, label: { HStack { Image(systemName: "teddybear.fill") Text("Teddy") } }) .menuBarExtraStyle(.window) } }
- 点击图标显示的内容为 ContentView.swift 中定义的内容;
- 自定义状态栏显示的内容,这里添加了 Image 和 Text;
menuBarExtraStyle选择了window。- .menu: 将内容渲染为从菜单栏图标下拉的菜单,如下形式:
- .window: 将内容渲染在一个类似于弹出窗口的窗口中,如下形式:
⌘+r运行应用程序,可以看到状态栏出现了泰迪熊的图标和 Teddy ,点击也能看到显示 ContentView 的内容,我们就可以通过修改 ContentView.swift 自定义内容了,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30import SwiftUI struct ContentView: View { var body: some View { VStack { Image(systemName: "teddybear.fill") .imageScale(.large) .foregroundStyle(.tint) Text("hello, Teddy!") .font(.caption) .padding() Divider() HStack { Spacer() Button("Quit") { NSApplication.shared.terminate(nil) }.keyboardShortcut("q") } } .padding() } } #Preview { ContentView() }
- 修改为
Text("hello, Teddy!"); - 添加一个退出应用的按钮,并绑定了
⌘+q的快捷键。
🎉再次⌘+r运行,菜单栏是这样的:
🤔如果修改 menuBarExtraStyle为 .menu,则会按照菜单形式渲染内容:
🖌️.menuBarExtraStyle为.menu时某些控件可能会失效😂,只能显示内容(按照菜单项高度、图像以 template 方式渲染)无法交互🤯,比如 Stepper、Slider 等,像基本的 Button、Text、Divider、Image 等还是能正常显示的。
💡如果你想完全不受限制的定制自己希望显示的内容还是选.window吧!
实现Dock 栏不显示应用图标
导航栏选择 MenuBarAppDemo,Targets 选择 MenuBarAppDemo,找到 Info 下的 Custom macOS Application Target Properties,添加一项 Application is agent (UIElement) 并设置值为 YES。
⌘+r 运行应用程序,正常的话在 Dock 栏就看不到应用程序的图标了。
删除主窗口
直接删除 MenuBarAppDemoApp.swift 中的 windowGroup 内容就好了,最终代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17import SwiftUI @main struct MenuBarAppDemoApp: App { var body: some Scene { MenuBarExtra(content: { ContentView() }, label: { HStack { Image(systemName: "teddybear.fill") Text("Teddy") } }) .menuBarExtraStyle(.window) } }
⌘+r 运行应用程序,正常的话应用的主窗口也没有了。
🎉 至此,纯菜单栏小工具的架子也就搭好了,后面根据自己的需要调整 ContentView 内容就好啦,是不是很简单🚀!
附件
本文 Demo 代码: MenuBarAppDemo。
总结
本文的方式是纯 SwiftUI 的实现方式,受到系统版本的限制,大家根据自己的 App 的情况酌情选择方案!除了macOS 开发之菜单栏形式的状态栏小工具讲中使用 Appkit 方式之外,也有 SwiftUI + Appkit 的方式(如下代码),不管用什么方式自己用着顺手、满足功能和开发需求就好,不必过于纠结!我个人更倾向于纯 SwiftUI 的方式(因为这是 Apple 发力的方向,路久但可期🔥),相信你也会有自己明确的选择!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41import SwiftUI @main struct MenuBarAppDemoApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { // 保留 Setting 声明以免报错 Settings { } } } class AppDelegate: NSObject, NSApplicationDelegate { private var statusItem: NSStatusItem! func applicationDidFinishLaunching(_ notification: Notification) { self.statusItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength)) // 定义菜单项 let menu = NSMenu() let editMenuItem = NSMenuItem() editMenuItem.title = "Quit" editMenuItem.action = #selector(NSApplication.terminate(_:)) editMenuItem.keyEquivalent = "q" menu.addItem(editMenuItem) self.statusItem.menu = menu // 定义按钮图标 if let button = self.statusItem.button { button.image = NSImage(systemSymbolName: "teddybear.fill", accessibilityDescription: "teddy") // 如果实现类似于 MenuBarEtraStyle.window 的效果 // 1. 添加开关 Popover 的实现 // 2. 将 Popover 的内容设置为 ContenView // 3. 去掉上面定义菜单项内容 } } }



