SwiftUI构建macOS菜单栏应用:实时监控AI API使用状态
在 macOS 开发中将常用工具或信息集成到菜单栏Menu Bar是一种提升效率的经典做法。对于频繁使用 AI 编程助手如 Claude Code 或基于 Codex 模型的服务的开发者而言实时查看 API 调用额度、剩余次数或使用状态无需频繁切换应用或打开浏览器能显著减少工作流的中断。一个常驻菜单栏的轻量级应用正是解决这一痛点的优雅方案。本文将带你从零开始使用 SwiftUI 构建一个原生 macOS 菜单栏应用核心功能是监控并展示 Claude Code 或类似 Codex 服务的 API 使用限制。我们将涵盖从项目创建、菜单栏图标设置、网络请求处理、数据解析与展示到最终打包的完整流程。无论你是想学习 SwiftUI 在 macOS 上的实践还是希望为自己常用的服务打造一个效率工具这篇文章都将提供清晰的路径和可运行的代码示例。1. 理解 macOS 菜单栏应用与 SwiftUI 框架在开始编码前需要明确两个核心概念macOS 菜单栏应用的基本形态以及 SwiftUI 如何适配这种形态。1.1 什么是 macOS 菜单栏应用macOS 菜单栏应用有时被称为“状态栏应用”或“Menu Bar Extra”其主界面并不表现为一个独立的窗口而是以一个图标的形式常驻在屏幕右上角的系统菜单栏中。用户点击这个图标会弹出一个下拉菜单或一个浮动的面板Popover应用的所有交互都发生在这个弹出区域。这类应用通常用于显示系统状态如网络、电池、快速启动任务或展示实时信息其特点是轻量、常驻且不占用 Dock 栏空间。从技术实现看它本质上仍然是一个标准的 macOS 应用.app包只是其生命周期管理和界面呈现方式与普通窗口应用不同。在 Xcode 中创建项目时我们选择的是“macOS”平台下的“App”模板然后通过代码将其主界面配置为菜单栏项目。1.2 SwiftUI 在 macOS 开发中的角色SwiftUI 是苹果推出的声明式 UI 框架它允许开发者用简洁的代码描述用户界面。在 macOS 上SwiftUI 可以用于构建各种类型的应用界面包括窗口内容和菜单栏弹出面板。对于菜单栏应用我们主要用 SwiftUI 来定义点击图标后弹出的那个面板Popover的内容。相较于传统的 AppKit使用NSStatusItem和NSMenu用 SwiftUI 构建菜单栏面板更加直观和高效特别是当面板内容包含复杂布局、列表或交互控件时。SwiftUI 的状态管理State,ObservedObject也能很好地与菜单栏应用的实时数据更新需求相结合。2. 环境准备与项目创建在开始编写代码之前请确保你的开发环境已就绪。2.1 开发环境要求组件要求说明操作系统macOS 12 (Monterey) 或更高版本SwiftUI 对 macOS 的完整支持需要较新的系统。建议使用 macOS 13 (Ventura) 或 14 (Sonoma) 以获得最佳体验。开发工具Xcode 14 或更高版本本文示例基于 Xcode 15。你可以在 Mac App Store 免费下载和更新 Xcode。编程语言Swift 5.7SwiftUI 框架与 Swift 语言版本紧密相关。目标部署macOS 11.0在项目设置中可以调整 Deployment Target 以支持更旧的系统但某些 SwiftUI 特性可能受限。注意如果你在安装 Xcode 或创建项目时遇到问题如搜索材料中提到的“Xcode 打开闪崩”请首先检查 macOS 系统版本与 Xcode 版本的兼容性。可以访问苹果开发者官网查看官方兼容性列表。通常解决方法是更新 macOS 系统到最新稳定版或下载与当前系统匹配的 Xcode 版本。2.2 创建 SwiftUI macOS 项目打开 Xcode在欢迎界面选择“Create New Project...”或通过菜单栏File-New-Project...。在模板选择器中选择“macOS”平台然后选中“App”模板点击“Next”。填写项目信息Product Name:AIUsageMenuBar(或其他你喜欢的名称)。Team: 选择你的开发者账户如果没有可以先选择“None”但真机调试和发布需要。Organization Identifier: 通常使用反域名格式如com.yourname。Interface: 确保选择SwiftUI。Language: 确保选择Swift。取消勾选 “Include Tests”以保持项目简洁。点击“Next”选择项目存储的位置然后点击“Create”。项目创建完成后你会看到熟悉的ContentView.swift文件。默认情况下这是一个窗口应用。我们的第一步是将其改造为菜单栏应用。3. 构建菜单栏应用骨架我们将修改应用入口和主视图使其不显示主窗口而是在菜单栏添加一个图标。3.1 修改应用入口main与App协议在 SwiftUI 的生命周期中使用main标记的结构体是应用的入口。我们需要修改自动生成的AIUsageMenuBarApp结构体。找到并打开AIUsageMenuBarApp.swift文件将其内容替换为以下代码import SwiftUI main struct AIUsageMenuBarApp: App { // 使用 StateObject 来创建并持有菜单栏状态管理器它将在整个应用生命周期内存在 StateObject private var appState AppState() var body: some Scene { // 删除默认的 WindowGroup我们不需要主窗口 // WindowGroup { // ContentView() // } // 添加一个 Settings 场景用于响应 Cmd, 快捷键打开偏好设置可选 Settings { SettingsView() } // 菜单栏场景是核心 MenuBarExtra(AI Usage, systemImage: brain.head.profile) { // 这里是点击菜单栏图标后弹出的内容视图 ContentView() .environmentObject(appState) // 注入状态 } .menuBarExtraStyle(.window) // 使用窗口样式的弹出面板而非纯菜单列表 } } // 应用状态管理器用于管理全局数据如API状态、使用量等 class AppState: ObservableObject { Published var usageInfo: UsageInfo? Published var isLoading false Published var lastUpdated: Date? // 可以在这里添加更多全局状态如API密钥、配置等 } // 一个简单的数据模型用于存储使用量信息 struct UsageInfo: Codable { let used: Int let limit: Int let resetDate: Date? var percentage: Double { Double(used) / Double(limit) } }关键解释MenuBarExtra: 这是 SwiftUI 中用于创建菜单栏项目的新 APImacOS 13。第一个参数是辅助功能标签第二个参数systemImage使用了 SF Symbols 中的图标。你可以替换为其他图标如chart.bar,number。.menuBarExtraStyle(.window): 这行代码至关重要。它将弹出内容定义为一个可自由布局的浮动窗口Popover而不是一个简单的菜单列表。这样我们就可以在里面使用任何 SwiftUI 视图。AppState: 这是一个遵循ObservableObject的类用于集中管理应用的状态。Published属性包装器使得当其值改变时所有依赖它的 SwiftUI 视图都会自动更新。StateObject和EnvironmentObject: 我们在应用入口创建AppState实例并通过.environmentObject()将其注入到视图环境中。这样在视图树中的任何子视图都可以通过EnvironmentObject来访问这个共享状态。3.2 设计主内容视图ContentView接下来我们设计点击菜单栏图标后弹出的面板内容。修改ContentView.swift文件import SwiftUI struct ContentView: View { // 通过环境对象获取全局状态 EnvironmentObject var appState: AppState var body: some View { VStack(alignment: .leading, spacing: 16) { // 标题区域 HStack { Image(systemName: sparkles) .font(.title2) .foregroundColor(.blue) Text(AI Usage Status) .font(.headline) Spacer() // 刷新按钮 Button(action: { Task { await fetchUsageData() } }) { Image(systemName: arrow.clockwise) } .buttonStyle(.plain) .disabled(appState.isLoading) // 设置按钮可选可跳转到Settings视图 Button(action: { // 打开系统偏好设置窗口的一种方式 NSApp.sendAction(Selector((showSettingsWindow:)), to: nil, from: nil) }) { Image(systemName: gear) } .buttonStyle(.plain) } .padding(.horizontal) .padding(.top) Divider() // 数据展示区域 if appState.isLoading { ProgressView() .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let info appState.usageInfo { VStack(alignment: .leading, spacing: 12) { // 使用量文本 HStack { Text(Used: **\(info.used)**) Spacer() Text(Limit: **\(info.limit)**) } .font(.body) // 进度条 ProgressView(value: info.percentage) .progressViewStyle(.linear) .tint(colorForPercentage(info.percentage)) // 百分比文本 Text(\(Int(info.percentage * 100))%) .font(.caption) .foregroundColor(.secondary) // 重置时间如果有 if let resetDate info.resetDate { HStack { Image(systemName: clock) .font(.caption) Text(Resets: \(resetDate, style: .relative)) .font(.caption) } .foregroundColor(.secondary) } } .padding(.horizontal) } else { // 无数据或错误状态 VStack(spacing: 10) { Image(systemName: exclamationmark.triangle) .font(.largeTitle) .foregroundColor(.orange) Text(No data available) .font(.body) Text(Check your configuration or network.) .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) } Divider() // 底部操作区域 HStack { Button(Quit) { NSApplication.shared.terminate(nil) } .buttonStyle(.bordered) Spacer() if let updated appState.lastUpdated { Text(Updated: \(updated, formatter: dateFormatter)) .font(.caption2) .foregroundColor(.gray) } } .padding(.horizontal) .padding(.bottom) } .frame(width: 300, height: 220) // 设置弹出面板的固定尺寸 .onAppear { // 视图出现时自动获取一次数据 Task { await fetchUsageData() } } } // 根据使用百分比返回进度条颜色 private func colorForPercentage(_ percentage: Double) - Color { switch percentage { case 0..0.7: return .green case 0.7..0.9: return .yellow default: return .red } } // 获取使用量数据的异步方法 private func fetchUsageData() async { // 在主线程更新加载状态 await MainActor.run { appState.isLoading true } // 模拟网络请求延迟 try? await Task.sleep(nanoseconds: 500_000_000) // 0.5秒 // 这里是模拟数据实际项目中应替换为真实的网络请求 let mockInfo UsageInfo(used: 342, limit: 500, resetDate: Date().addingTimeInterval(86400)) // 24小时后重置 await MainActor.run { appState.usageInfo mockInfo appState.lastUpdated Date() appState.isLoading false } // 实际网络请求代码示例占位 /* guard let url URL(string: https://api.your-ai-service.com/usage) else { return } var request URLRequest(url: url) request.setValue(Bearer YOUR_API_KEY, forHTTPHeaderField: Authorization) do { let (data, _) try await URLSession.shared.data(for: request) let decodedInfo try JSONDecoder().decode(UsageInfo.self, from: data) await MainActor.run { appState.usageInfo decodedInfo appState.lastUpdated Date() appState.isLoading false } } catch { print(Failed to fetch usage: \(error)) await MainActor.run { appState.isLoading false // 可以在这里更新错误状态 } } */ } } // 一个简单的日期格式化器用于底部时间显示 private let dateFormatter: DateFormatter { let formatter DateFormatter() formatter.timeStyle .short formatter.dateStyle .none return formatter }() // 预览提供程序 struct ContentView_Previews: PreviewProvider { static var previews: some View { // 为预览提供模拟状态 let mockState AppState() mockState.usageInfo UsageInfo(used: 420, limit: 500, resetDate: Date().addingTimeInterval(3600)) mockState.lastUpdated Date() return ContentView() .environmentObject(mockState) .frame(width: 300, height: 220) } }视图结构解析这个ContentView被设计为一个垂直栈VStack包含以下几个部分标题栏显示应用图标和名称右侧有刷新和设置按钮。数据区域加载中显示一个进度指示器。有数据展示已用/总量、彩色进度条、百分比和重置倒计时。无数据/错误显示提示图标和文字。底部栏包含退出按钮和最后更新时间。fetchUsageData方法目前使用了模拟数据。在实际集成时你需要在此处发起真实的网络请求。3.3 创建简单的设置视图SettingsView为了让应用更完整我们添加一个简单的设置视图用于未来放置 API 配置等选项。创建一个新的 SwiftUI 视图文件SettingsView.swiftimport SwiftUI struct SettingsView: View { // 这里可以使用 AppStorage 来持久化用户配置 AppStorage(apiEndpoint) private var apiEndpoint: String https://api.example.com AppStorage(refreshInterval) private var refreshInterval: Double 300 // 默认5分钟单位秒 var body: some View { Form { TextField(API Endpoint:, text: $apiEndpoint) .textFieldStyle(RoundedBorderTextFieldStyle()) HStack { Text(Auto-refresh every:) Picker(, selection: $refreshInterval) { Text(1 min).tag(60.0) Text(5 min).tag(300.0) Text(15 min).tag(900.0) Text(30 min).tag(1800.0) Text(Never).tag(0.0) } .pickerStyle(MenuPickerStyle()) .frame(width: 120) } Divider().padding(.vertical, 5) Text(Settings take effect after restart.) .font(.caption) .foregroundColor(.secondary) } .padding() .frame(width: 400, height: 200) } }现在运行应用CmdR。你会看到菜单栏右侧出现一个大脑图标。点击它会弹出我们设计的面板。点击面板上的齿轮图标会打开一个独立的设置窗口。这个应用的基本骨架已经完成。4. 集成真实的 AI 服务 API模拟数据只能用于演示。要让应用真正有用必须连接真实的 AI 服务 API。这里以 Claude API 为例请注意Claude API 的具体端点、认证方式和响应格式请以其官方文档为准。4.1 准备网络请求层我们创建一个专门的数据管理器DataManager负责处理所有网络请求和数据解析。创建一个新的 Swift 文件DataManager.swiftimport Foundation import Combine class DataManager { static let shared DataManager() private init() {} private let decoder: JSONDecoder { let decoder JSONDecoder() decoder.keyDecodingStrategy .convertFromSnakeCase // 如果API返回snake_case自动转camelCase decoder.dateDecodingStrategy .iso8601 // 根据API返回的日期格式调整 return decoder }() // 示例获取Claude API使用情况 // 注意此URL、参数和响应结构为示例需根据Claude官方API文档调整 func fetchClaudeUsage(apiKey: String) async throws - UsageInfo { guard let url URL(string: https://api.anthropic.com/v1/usage) else { throw URLError(.badURL) } var request URLRequest(url: url) request.httpMethod GET request.setValue(application/json, forHTTPHeaderField: Content-Type) request.setValue(2023-06-01, forHTTPHeaderField: anthropic-version) // 示例版本头 request.setValue(Bearer \(apiKey), forHTTPHeaderField: Authorization) // Claude API 可能还需要 x-api-key 头请查阅最新文档 // request.setValue(apiKey, forHTTPHeaderField: x-api-key) let (data, response) try await URLSession.shared.data(for: request) guard let httpResponse response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { // 尝试解析错误信息 if let errorResponse try? JSONDecoder().decode(APIError.self, from: data) { throw NetworkError.apiError(message: errorResponse.error?.message ?? Unknown API error) } throw NetworkError.invalidResponse(statusCode: (response as? HTTPURLResponse)?.statusCode ?? -1) } // 假设API返回格式为 { total_usage: 150, usage_limit: 1000, reset_time: 2023-10-27T00:00:00Z } let apiResponse try decoder.decode(ClaudeAPIResponse.self, from: data) return UsageInfo(used: apiResponse.totalUsage, limit: apiResponse.usageLimit, resetDate: apiResponse.resetTime) } // 你可以添加其他服务的方法例如OpenAI Codex // func fetchOpenAIUsage(apiKey: String) async throws - UsageInfo { ... } } // 假设的 Claude API 响应结构 struct ClaudeAPIResponse: Codable { let totalUsage: Int let usageLimit: Int let resetTime: Date? } // 通用的 API 错误响应结构示例 struct APIError: Codable { let error: ErrorDetail? } struct ErrorDetail: Codable { let message: String let type: String? } // 自定义错误类型 enum NetworkError: Error, LocalizedError { case invalidResponse(statusCode: Int) case apiError(message: String) case decodingError case invalidAPIKey var errorDescription: String? { switch self { case .invalidResponse(let code): return Server returned an invalid response (Code: \(code)). case .apiError(let message): return API Error: \(message) case .decodingError: return Failed to parse the server response. case .invalidAPIKey: return The provided API key is invalid or missing. } } }4.2 在 AppState 中集成 DataManager 并添加定时刷新现在修改AppState类使其能够调用DataManager并管理定时任务。import SwiftUI import Combine class AppState: ObservableObject { Published var usageInfo: UsageInfo? Published var isLoading false Published var lastUpdated: Date? Published var errorMessage: String? // 存储用户配置 AppStorage(claudeAPIKey) var claudeAPIKey: String AppStorage(autoRefreshEnabled) var autoRefreshEnabled: Bool true AppStorage(refreshInterval) var refreshInterval: Double 300 // 秒 private var timer: Timer? private var cancellables SetAnyCancellable() init() { // 监听刷新间隔设置的变化以重启定时器 $refreshInterval .dropFirst() // 忽略初始值 .sink { [weak self] _ in self?.setupTimer() } .store(in: cancellables) $autoRefreshEnabled .dropFirst() .sink { [weak self] enabled in if enabled { self?.setupTimer() } else { self?.timer?.invalidate() self?.timer nil } } .store(in: cancellables) } deinit { timer?.invalidate() } MainActor func loadUsageData() async { // 防止重复加载 guard !isLoading else { return } isLoading true errorMessage nil // 检查API密钥 guard !claudeAPIKey.isEmpty else { errorMessage Please set your Claude API Key in Settings. isLoading false return } do { let info try await DataManager.shared.fetchClaudeUsage(apiKey: claudeAPIKey) self.usageInfo info self.lastUpdated Date() } catch { errorMessage error.localizedDescription print(Failed to load usage data: \(error)) } isLoading false } private func setupTimer() { // 先取消旧的定时器 timer?.invalidate() guard autoRefreshEnabled, refreshInterval 0 else { return } // 创建新的定时器在主线程触发 timer Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { [weak self] _ in Task { MainActor in await self?.loadUsageData() } } } // 手动触发刷新供视图调用 func manualRefresh() { Task { await loadUsageData() } } }4.3 更新 ContentView 以使用新的状态和方法现在修改ContentView.swift中的fetchUsageData方法改为调用AppState的方法并改进错误显示。struct ContentView: View { EnvironmentObject var appState: AppState var body: some View { VStack(alignment: .leading, spacing: 16) { // ... 标题区域保持不变 ... Divider() // 数据展示区域 if appState.isLoading { // ... 加载指示器 ... } else if let error appState.errorMessage { // 错误状态视图 VStack(spacing: 10) { Image(systemName: xmark.octagon) .font(.largeTitle) .foregroundColor(.red) Text(Error) .font(.headline) Text(error) .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } else if let info appState.usageInfo { // ... 数据展示视图 ... } else { // 初始无数据状态例如API密钥未设置 VStack(spacing: 10) { Image(systemName: key) .font(.largeTitle) .foregroundColor(.blue) Text(Setup Required) .font(.headline) Text(Open Settings to configure your API key.) .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) } // ... 底部区域保持不变 ... } .frame(width: 300, height: 240) // 稍微调高以容纳错误信息 .onAppear { // 视图出现时自动获取一次数据 appState.manualRefresh() } } // ... colorForPercentage 等方法保持不变 ... }同时更新刷新按钮的动作直接调用appState.manualRefresh()。4.4 更新 SettingsView 以配置 API 密钥最后更新SettingsView.swift添加 API 密钥的配置项。重要在实际应用中应考虑使用 Keychain 等更安全的方式存储 API 密钥AppStorage以明文存储在UserDefaults中安全性较低此处仅作演示。struct SettingsView: View { EnvironmentObject var appState: AppState var body: some View { Form { Section(header: Text(Claude API Configuration)) { SecureField(API Key:, text: $appState.claudeAPIKey) .textFieldStyle(RoundedBorderTextFieldStyle()) Text(Your API key is stored locally and only used to request usage data.) .font(.caption) .foregroundColor(.secondary) } Section(header: Text(Refresh Settings)) { Toggle(Auto-refresh, isOn: $appState.autoRefreshEnabled) if appState.autoRefreshEnabled { HStack { Text(Interval:) Picker(, selection: $appState.refreshInterval) { Text(1 min).tag(60.0) Text(5 min).tag(300.0) Text(15 min).tag(900.0) Text(30 min).tag(1800.0) } .pickerStyle(MenuPickerStyle()) .frame(width: 120) } } } Section { Button(Test Connection) { Task { await appState.loadUsageData() } } } } .padding() .frame(width: 400, height: 250) } }至此一个功能相对完整的菜单栏应用就构建完成了。它可以从 Claude API 获取使用量数据支持自动刷新并提供了基本的设置界面。5. 构建、打包与分发开发完成后你可能希望将应用分享给他人或自行使用。5.1 配置应用图标和元数据在项目导航器中点击项目根目录最顶层的蓝色图标。在TARGETS下选择你的应用目标如AIUsageMenuBar。在General标签页你可以设置Display Name在 Dock 和菜单栏提示中显示的名称、Bundle Identifier等。在Signing Capabilities标签页确保选择了正确的团队以进行代码签名。对于个人使用可以选择“None”但这样应用可能无法在非开发机器上运行。要设置应用图标需要准备一个.icns文件。你可以使用工具将 PNG 图片转换为 icns 格式然后在Assets.xcassets中替换AppIcon。5.2 构建归档与导出在 Xcode 菜单栏选择Product-Scheme-Edit Scheme...确保Run和Archive的Build Configuration都是Release。选择Product-Archive。Xcode 会编译一个 Release 版本的应用并打开 Organizer 窗口。在 Organizer 窗口中选中刚刚创建的归档点击右侧的Distribute App。选择 “Copy App”然后选择一个输出目录。这将在该目录下生成一个.app文件这就是可以分发的独立应用。5.3 设置为登录启动项为了让应用在开机时自动启动可以将生成的.app文件拖入系统设置-通用-登录项中。6. 常见问题与排查在开发和使用过程中你可能会遇到以下问题。6.1 应用启动后菜单栏不显示图标现象可能原因检查与解决运行后菜单栏无图标1.MenuBarExtra只在 macOS 13 有效。2. 项目 Deployment Target 设置过低。3. 模拟器或某些系统配置问题。1. 确认系统版本 ≥ macOS 13。2. 检查项目Deployment Target≥ 13.0。3. 尝试在真机非模拟器上运行。如果使用NSStatusItem旧 API 则无此限制。6.2 网络请求失败现象可能原因检查与解决面板一直显示“Loading”或错误信息1. API 密钥错误或未设置。2. 网络连接问题。3. API 端点 URL 错误。4. 请求头或参数不符合 API 要求。5. 应用缺少网络权限沙盒。1. 在设置中确认 API 密钥正确无误。2. 检查DataManager中的 URL 和请求头是否与官方文档一致。3. 在fetchClaudeUsage方法中添加print语句输出请求的 URL 和响应状态码、原始数据进行调试。4. 如果应用启用了沙盒Sandbox需要在Signing Capabilities中添加Outgoing Connections (Client)权限。6.3 数据解析错误现象可能原因检查与解决应用崩溃或数据显示异常1. API 返回的 JSON 结构与ClaudeAPIResponse模型不匹配。2. 日期格式解析失败。1. 使用类似 Postman 的工具直接调用 API确认返回的 JSON 结构。2. 调整JSONDecoder的keyDecodingStrategy和dateDecodingStrategy。3. 在do-catch块中捕获DecodingError并打印详细错误信息。6.4 内存泄漏或性能问题现象可能原因检查与解决应用运行一段时间后变卡或意外退出1. 定时器未正确销毁导致重复创建。2. 网络请求回调中强引用导致循环引用。1. 确保在AppState的deinit或setupTimer开始时invalidate旧的定时器。2. 在闭包中使用[weak self]捕获列表。3. 使用 Xcode 的 Instruments 工具检查内存分配和泄漏。7. 扩展方向与最佳实践这个基础应用可以沿多个方向进行扩展和优化。7.1 功能扩展建议多服务支持在DataManager中添加fetchOpenAIUsage、fetchGeminiUsage等方法并在 UI 上提供切换开关同时监控多个 AI 服务的使用情况。通知提醒当使用量超过特定阈值如 80%、90%时通过UserNotifications框架发送本地通知。更丰富的展示在菜单栏图标本身上显示使用百分比如42%这需要自定义NSStatusItem的视图MenuBarExtra的图标是静态的。历史记录将每次获取的使用量数据持久化到本地数据库如 SQLite 通过GRDB或 Core Data并提供一个图表视图来展示使用趋势。快捷键支持为刷新操作分配一个全局快捷键。7.2 安全与配置最佳实践安全存储 API 密钥使用苹果的 Keychain Services (Securityframework) 替代AppStorage来存储敏感的 API 密钥。配置外部化考虑将 API 端点等配置信息放在一个Config.plist文件中便于不同环境开发、生产切换。错误处理与重试为网络请求添加指数退避等重试机制提高在临时网络故障下的健壮性。日志记录集成一个轻量级日志框架如OSLog记录关键操作和错误便于后期排查问题。7.3 针对不同 macOS 版本的兼容性本文核心使用了 macOS 13 的MenuBarExtraAPI。如果你的应用需要支持更早的系统版本如 macOS 11, 12则需要回退到传统的 AppKit 方式创建菜单栏项目。这涉及到使用NSStatusItem和将 SwiftUI 视图托管到NSHostingView中。虽然代码会更复杂但可以覆盖更广的用户群。在项目初期就决定好最低支持的系统版本是关键的产品决策。通过以上步骤你不仅构建了一个实用的 macOS 菜单栏应用也实践了 SwiftUI 状态管理、异步网络请求、菜单栏交互等核心开发技能。这个项目可以作为模板快速适配其他需要实时监控状态的 API 服务。