从零搭建 SwiftUI macOS 菜单栏应用

macOS 菜单栏应用是一种轻量级的桌面工具,常驻于屏幕顶部的菜单栏中,用户可以随时点击图标快速访问功能。得益于 macOS 13 Ventura 引入的 MenuBarExtra API,使用 SwiftUI 构建一个菜单栏应用变得前所未有的简单。本文将带你从零开始,逐步完成一个完整的菜单栏应用。

为什么选择菜单栏应用?

与传统的窗口应用不同,菜单栏应用具有独特的优势:

  • 常驻可见——始终显示在菜单栏中,用户无需切换窗口即可访问
  • 轻量快捷——没有复杂的窗口层级,交互路径极短
  • 不占空间——不会在 Dock 中占用位置,适合后台常驻工具

常见的菜单栏应用包括天气工具、剪贴板管理器、番茄钟、系统监控等。本文我们将构建一个简洁的倒数日计数器,用于跟踪重要日期。

项目初始化

打开 Xcode,创建一个新的 macOS App 项目:

  1. 选择 macOS → App 模板
  2. Interface 选择 SwiftUI
  3. Language 选择 Swift

在项目设置中,将 Application Scene Manifest 中的 Application has menu bar 取消勾选——因为我们不需要传统的菜单栏,只需要一个菜单栏图标。

App 入口:MenuBarExtra

在 App 的入口文件中,我们使用 MenuBarExtra 场景替代默认的 WindowGroup

import SwiftUI

@main
struct CountdownApp: App {
    var body: some Scene {
        MenuBarExtra("倒数日", systemImage: "calendar.badge.clock") {
            ContentView()
        }
        .menuBarExtraStyle(.window)
    }
}

这里有几个关键点:

  • MenuBarExtra 的第一个参数是辅助功能标签,第二个是 SF Symbol 图标名称
  • .menuBarExtraStyle(.window) 让点击后弹出的是一个 SwiftUI 视图窗口,而非默认的菜单样式
  • 你也可以使用 .menu 样式来呈现一个传统的下拉菜单

构建内容视图

接下来我们实现 ContentView,展示即将到来的重要日期和剩余天数:

struct ContentView: View {
    @State private var events: [CountdownEvent] = [
        CountdownEvent(name: "春节", date: Calendar.current.date(from: DateComponents(year: 2026, month: 2, day: 17))!),
        CountdownEvent(name: "WWDC", date: Calendar.current.date(from: DateComponents(year: 2026, month: 6, day: 1))!),
    ]

    var body: some View {
        VStack(spacing: 0) {
            Text("倒数日")
                .font(.headline)
                .padding()

            Divider()

            ForEach(events) { event in
                CountdownRow(event: event)
                if event.id != events.last?.id {
                    Divider().padding(.horizontal)
                }
            }

            Divider()

            Button("退出") {
                NSApplication.shared.terminate(nil)
            }
            .padding(8)
        }
        .frame(width: 280)
    }
}

struct CountdownEvent: Identifiable {
    let id = UUID()
    let name: String
    let date: Date

    var daysRemaining: Int {
        Calendar.current.dateComponents([.day], from: Date(), to: date).day ?? 0
    }
}

struct CountdownRow: View {
    let event: CountdownEvent

    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 2) {
                Text(event.name)
                    .font(.system(size: 14, weight: .medium))
                Text(event.date, style: .date)
                    .font(.system(size: 11))
                    .foregroundColor(.secondary)
            }

            Spacer()

            Text("\(event.daysRemaining) 天")
                .font(.system(size: 16, weight: .bold, design: .rounded))
                .foregroundColor(event.daysRemaining <= 7 ? .orange : .blue)
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 10)
    }
}

设置与偏好

一个完整的菜单栏应用通常需要设置窗口。我们使用 SwiftUI 的 Settings 场景来实现:

var body: some Scene {
    MenuBarExtra("倒数日", systemImage: "calendar.badge.clock") {
        ContentView()
    }
    .menuBarExtraStyle(.window)

    Settings {
        SettingsView()
    }
}

SettingsView 可以通过菜单栏的 "倒数日 → 设置..." 或快捷键 ⌘, 打开。在其中我们可以让用户自定义要跟踪的事件列表,并使用 @AppStorage 持久化数据。

定时更新

由于天数每天会变化,我们需要在午夜自动刷新界面。监听 NSCalendarDayChanged 通知即可:

struct ContentView: View {
    @State private var events: [CountdownEvent] = loadEvents()
    @State private var refreshID = UUID() // force refresh

    var body: some View {
        // ... view content
        .id(refreshID)
        .onReceive(NotificationCenter.default.publisher(for: .NSCalendarDayChanged)) { _ in
            refreshID = UUID()
        }
    }
}

发布到 App Store

要将应用发布到 Mac App Store,需要完成以下步骤:

  1. 在 Xcode 中配置 Signing & Capabilities,选择你的开发团队
  2. 在 App Store Connect 中创建新 App 记录
  3. 配置 App Sandbox 权限(菜单栏应用通常只需要最低权限)
  4. Archive 并上传到 App Store Connect
  5. 填写隐私政策、截图和描述
  6. 提交审核

注意:菜单栏应用在上架时需要提供清晰的用户引导,因为用户可能不知道如何找到菜单栏中的新图标。建议在首次启动时提供一个引导提示。

总结

MenuBarExtra 是 SwiftUI 在 macOS 平台上的一大进步。过去要用 NSStatusBarButton 和 AppKit 混合开发,现在几行 SwiftUI 代码就能搞定。如果你是 SwiftUI 开发者,不妨动手试一试,把经常用到的小工具做成菜单栏应用——这是一种非常适合个人开发者切入 macOS 生态的方式。

完整代码已托管在 GitHub,欢迎参考和 Star ⭐