
macOS 开发 - 创建 SwiftUI + AppKit 状态栏应用(popover视图)
🎯简单几步打造 macOS 原生状态栏应用!从零开始构建一个专业的状态栏 Popover 工具,使用 SwiftUI 构建界面 + AppKit 实现系统集成。包含完整可运行代码、全屏模式兼容方案、键盘快捷键支持。适合想要创建菜单栏工具的 macOS 开发者 🚀
本文将带你从零开始,逐步创建一个完整的 macOS 状态栏 popover 应用。我们将使用 SwiftUI + AppKit 混合开发模式,每一步都提供可运行的代码,让你能够跟着操作并实时验证效果。
为什么选择 SwiftUI + AppKit 混合开发
SwiftUI 的优势:
- 声明式 UI 开发,代码简洁高效
- 强大的状态管理和响应式编程
- 优秀的动画和视觉效果支持
AppKit 的必要性:
- 完整的状态栏 API 支持
- 精确的窗口层级和空间控制
- 系统级事件监听和处理
混合开发的价值:
- 充分发挥两个框架的优势
- 渐进式迁移,降低开发风险
- 更好的系统集成能力
注意: 如果只需要简单的菜单功能,macOS 13+ 的 MenuBarExtra 可能更适合。但对于需要复杂交互的 popover 界面,SwiftUI + AppKit 是更稳定的选择。第一步:创建 Xcode 项目
1. 新建项目
- 打开 Xcode,选择 "File" > "New" > "Project"
- 选择 "macOS" > "App"
- 填写项目信息:
- Product Name:
StatusBarDemo - Interface:
SwiftUI - Language:
Swift - Use Core Data: 不选择
- Product Name:
- 选择保存位置并创建项目
2. 项目初始设置
创建项目后,你会看到默认的 StatusBarDemoApp.swift 文件。我们需要对其进行修改。
首先,将默认的 StatusBarDemoApp.swift 内容替换为:
1 2 3 4 5 6 7 8 9 10import SwiftUI @main struct StatusBarDemoApp: App { var body: some Scene { WindowGroup { ContentView() } } }
此时运行项目(⌘+R),你应该能看到一个标准的 macOS 窗口。
第二步:配置应用为状态栏模式
修改 StatusBarDemoApp.swift
我们需要将应用配置为状态栏模式。将 StatusBarDemoApp.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 28import SwiftUI import AppKit @main struct StatusBarDemoApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { Settings { EmptyView() // 防止自动生成主窗口 } } } final class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { // 设置为辅助应用(不显示 Dock 图标) NSApp.setActivationPolicy(.accessory) print("✅ 状态栏应用启动成功") // 这里我们稍后会添加状态栏初始化代码 } func applicationWillTerminate(_ notification: Notification) { print("📱 应用即将退出") // 这里我们稍后会添加清理代码 } }
运行并验证
按 ⌘+R 运行应用:
- 你不应该看到任何窗口
- Dock 中不应该有应用图标
- 在 Xcode 的控制台中应该能看到 "✅ 状态栏应用启动成功"
如果一切正常,说明应用已经成功配置为状态栏模式。
第三步:创建状态栏图标
1. 创建状态栏控制器文件
在 Xcode 中:
- 右键点击项目文件夹
- 选择 New File...
- 选择 macOS > Swift File
- 命名为
StatusBarController.swift - 点击 Create
2. 实现基础状态栏控制器
在 StatusBarController.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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45import AppKit import SwiftUI class StatusBarController { // 单例模式 static let shared = StatusBarController() private var statusItem: NSStatusItem? private init() {} /// 设置状态栏图标 func setupStatusBar() { // 创建状态栏项目 statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) // 确保成功创建 guard let statusButton = statusItem?.button else { print("❌ 无法创建状态栏按钮") return } // 设置图标(使用系统图标) statusButton.image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: "状态栏应用") // 设置点击事件 statusButton.action = #selector(statusBarClicked) statusButton.target = self print("✅ 状态栏图标创建成功") } /// 状态栏图标点击事件 @objc private func statusBarClicked() { print("🖱️ 状态栏图标被点击了!") } /// 清理状态栏 func cleanup() { if let statusItem = statusItem { NSStatusBar.system.removeStatusItem(statusItem) print("🧹 状态栏已清理") } } }
3. 在应用启动时初始化状态栏
修改 StatusBarDemoApp.swift 中的 AppDelegate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18final class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { // 设置为辅助应用(不显示 Dock 图标) NSApp.setActivationPolicy(.accessory) // 初始化状态栏 StatusBarController.shared.setupStatusBar() print("✅ 状态栏应用启动成功") } func applicationWillTerminate(_ notification: Notification) { // 清理资源 StatusBarController.shared.cleanup() print("📱 应用即将退出") } }
4. 运行并验证
按 ⌘+R 运行应用:
- 你应该在状态栏右侧看到一个星形图标 ⭐
- 点击图标,控制台应该显示 "🖱️ 状态栏图标被点击了!"
如果看到了状态栏图标并且点击有响应,说明基础功能已经实现!
第四步:创建 SwiftUI Popover 视图
1. 创建 Popover 内容视图
创建新文件 StatusBarPopView.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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125import SwiftUI struct StatusBarPopView: View { @State private var counter = 0 @State private var message = "欢迎使用状态栏应用!" var body: some View { VStack(spacing: 16) { // 标题区域 headerView Divider() // 主要内容 mainContent Divider() // 操作按钮 actionButtons } .padding(20) .frame(width: 300) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) } // 标题视图 private var headerView: some View { HStack { Image(systemName: "star.fill") .foregroundColor(.orange) .font(.title2) Text("我的状态栏应用") .font(.headline) .fontWeight(.medium) Spacer() Text("v1.0") .font(.caption) .foregroundColor(.secondary) } } // 主要内容区域 private var mainContent: some View { VStack(spacing: 12) { Text(message) .font(.body) .multilineTextAlignment(.center) .foregroundColor(.primary) // 计数器 HStack(spacing: 16) { Button(action: { counter -= 1 updateMessage() }) { Image(systemName: "minus.circle.fill") .font(.title2) .foregroundColor(.red) } .buttonStyle(.plain) Text("\(counter)") .font(.title) .fontWeight(.bold) .monospacedDigit() .frame(minWidth: 60) Button(action: { counter += 1 updateMessage() }) { Image(systemName: "plus.circle.fill") .font(.title2) .foregroundColor(.green) } .buttonStyle(.plain) } .padding() .background(.background.secondary, in: RoundedRectangle(cornerRadius: 8)) } } // 操作按钮区域 private var actionButtons: some View { HStack(spacing: 12) { Button("重置") { withAnimation(.spring()) { counter = 0 message = "计数器已重置" } } .buttonStyle(.bordered) Spacer() Button("退出应用") { NSApp.terminate(nil) } .buttonStyle(.bordered) .foregroundColor(.red) } } // 更新消息 private func updateMessage() { withAnimation(.easeInOut(duration: 0.3)) { if counter == 0 { message = "开始计数吧!" } else if counter > 0 { message = "很好!当前计数: \(counter)" } else { message = "负数了,要不要重置?" } } } } // 预览 #Preview { StatusBarPopView() }
2. 修改状态栏控制器以支持 Popover
更新 StatusBarController.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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73import AppKit import SwiftUI class StatusBarController { static let shared = StatusBarController() private var statusItem: NSStatusItem? private var popover: NSPopover? private init() {} func setupStatusBar() { // 创建状态栏项目 statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) guard let statusButton = statusItem?.button else { print("❌ 无法创建状态栏按钮") return } // 设置图标 statusButton.image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: "状态栏应用") statusButton.action = #selector(togglePopover) statusButton.target = self print("✅ 状态栏图标创建成功") } @objc private func togglePopover() { guard let statusButton = statusItem?.button else { return } if let popover = popover, popover.isShown { // 如果 popover 已显示,则隐藏 hidePopover() } else { // 显示 popover showPopover(relativeTo: statusButton) } } private func showPopover(relativeTo button: NSStatusBarButton) { // 创建 popover let newPopover = NSPopover() newPopover.behavior = .transient // 点击外部自动关闭 newPopover.animates = true newPopover.contentSize = NSSize(width: 300, height: 280) // 设置 SwiftUI 内容 let contentView = StatusBarPopView() newPopover.contentViewController = NSHostingController(rootView: contentView) self.popover = newPopover // 显示 popover newPopover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) print("✅ Popover 显示成功") } private func hidePopover() { popover?.performClose(nil) popover = nil print("📦 Popover 已隐藏") } func cleanup() { hidePopover() if let statusItem = statusItem { NSStatusBar.system.removeStatusItem(statusItem) print("🧹 状态栏已清理") } } }
3. 运行并测试
按 ⌘+R 运行应用:
- 点击状态栏的星形图标
- 应该会显示一个美观的 popover 窗口
- 尝试点击 +/- 按钮测试计数器功能
- 点击 popover 外部,窗口应该自动关闭
如果 popover 正常显示和交互,恭喜你已经完成了基础功能!
第五步:解决全屏模式显示问题
在某些情况下(特别是全屏应用场景),popover 可能无法正确显示。我们需要添加窗口层级配置。
更新 showPopover 方法
在 StatusBarController.swift 的 showPopover 方法后添加窗口配置方法:
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 33private func showPopover(relativeTo button: NSStatusBarButton) { // ... 前面的 popover 创建代码保持不变 ... // 显示 popover newPopover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) // 🔥 关键配置:确保在全屏模式下正确显示 configurePopoverWindow(newPopover) print("✅ Popover 显示成功") } /// 配置 popover 窗口属性 private func configurePopoverWindow(_ popover: NSPopover) { guard let window = popover.contentViewController?.view.window else { print("⚠️ 无法获取 popover 窗口") return } // 设置窗口层级为状态栏级别 window.level = .statusBar // 设置窗口行为:允许在所有空间显示,支持全屏辅助 window.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary] // 窗口关闭时不释放 window.isReleasedWhenClosed = false // 激活应用以确保获得焦点 NSApp.activate(ignoringOtherApps: true) print("🔧 Popover 窗口配置完成") }
第六步:添加键盘支持和事件监听
为了提升用户体验,我们添加 ESC 键关闭和点击外部关闭的功能。
完善状态栏控制器
在 StatusBarController 类中添加事件监听器存储和相关方法:
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56class StatusBarController { // ... 前面的属性定义 ... private var eventMonitors: [Any] = [] // 🆕 存储事件监听器 // ... 前面的 setupStatusBar、togglePopover 等方法保持不变 ... // 在 showPopover 方法中调用 setupEventMonitors() private func showPopover(relativeTo button: NSStatusBarButton) { // ... popover 创建和显示代码 ... // 配置窗口属性 configurePopoverWindow(newPopover) // 🆕 设置事件监听 setupEventMonitors() } /// 设置事件监听器 private func setupEventMonitors() { // ESC 键关闭 popover let localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in if event.keyCode == 53 { // ESC 键 self?.hidePopover() return nil } return event } // 点击外部关闭 popover let globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] _ in self?.hidePopover() } // 保存监听器引用 if let localMonitor = localMonitor { eventMonitors.append(localMonitor) } if let globalMonitor = globalMonitor { eventMonitors.append(globalMonitor) } print("🎧 事件监听器设置完成") } // 更新 hidePopover 方法 private func hidePopover() { popover?.performClose(nil) popover = nil // 🧹 清理事件监听器 eventMonitors.forEach { NSEvent.removeMonitor($0) } eventMonitors.removeAll() print("📦 Popover 已隐藏,监听器已清理") } }
第七步:测试和验证
最终测试清单
运行应用并进行以下测试:
- 基础功能测试:
- ✅ 状态栏图标正确显示
- ✅ 点击图标显示 popover
- ✅ popover 内容正确渲染
- ✅ 计数器功能正常工作
- 交互测试:
- ✅ 点击 popover 外部自动关闭
- ✅ 按 ESC 键关闭 popover
- ✅ 重复打开/关闭 popover 正常
- 全屏模式测试:
- ✅ 打开任意全屏应用(如全屏的浏览器)
- ✅ 点击状态栏图标,popover 能正常显示
- ✅ 在不同的桌面空间中测试
完整代码结构
此时你的项目应该包含以下文件:
1 2 3 4 5StatusBarDemo/ ├── StatusBarDemoApp.swift # 应用主入口 ├── StatusBarController.swift # 状态栏控制器 ├── StatusBarPopView.swift # Popover 内容视图 └── ContentView.swift # 默认视图(未使用)
常见问题与解决方案
Q: Popover 在某些情况下无法显示?
A: 检查窗口层级设置,确保 window.level 至少为 .statusBar。在全屏应用中可能需要设置为 .floating。
Q: 如何让状态栏图标支持拖拽?
A: 实现 NSStatusBarButton 的拖拽相关代理方法,或使用自定义的 NSView。
Q: 如何持久化用户设置?
A: 使用 @AppStorage 或 UserDefaults:
1@AppStorage("counterValue") private var counter = 0
Q: 如何在多显示器环境下正确显示?
A: 使用 collectionBehavior 的 .moveToActiveSpace 选项,确保 popover 跟随当前活跃的显示器。
Q: 应用启动时显示在 Dock 中?
A: 确保在 applicationDidFinishLaunching 中正确设置:
1NSApp.setActivationPolicy(.accessory)
Q: 如何自定义状态栏图标?
A: 将自定义图标添加到项目中,然后:
1 2 3 4 5// 使用项目中的图片 statusButton.image = NSImage(named: "custom-icon") // 或者使用 SF Symbols statusButton.image = NSImage(systemSymbolName: "your-symbol-name", accessibilityDescription: nil)
总结
通过 SwiftUI + AppKit 混合开发模式,我们可以创建功能强大且用户体验出色的 macOS 状态栏应用。关键要点包括:
- 正确的窗口配置:确保 popover 在所有情况下都能正常显示
- 完善的事件处理:提供符合用户预期的交互体验
- 良好的内存管理:避免内存泄漏和性能问题
- 版本兼容性:适配不同 macOS 版本的特性差异
这种开发模式既能享受 SwiftUI 的开发效率,又能充分利用 AppKit 的系统集成能力,是现代 macOS 应用开发的理想选择。



