三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于Swift与AppKit的macOS菜单栏应用开发:从自动化需求到原生实现

基于Swift与AppKit的macOS菜单栏应用开发:从自动化需求到原生实现

1. 从“想吃”到“一键下单”:一个吃货程序员的执念

作为一个资深吃货兼 macOS 重度用户,我经常遇到一个让人抓狂的场景:深夜写代码,突然馋虫上脑,想吃小龙虾。打开外卖 App,搜索、筛选、比价、凑单、领券……一套流程下来,可能半小时过去了,灵感没了,食欲也凉了半截。为什么不能像切换 Wi-Fi 或者调节音量一样,在菜单栏上点一下,就直接下单我最常吃的那家小龙虾呢?

这个想法在我脑子里盘旋了很久。直到最近,我决定不再忍受这种低效,动手打造一个名为OpenClaw的 macOS 应用。它的核心目标极其简单:把“点小龙虾”这个高频、刚需的动作,变成一个固定在菜单栏上的、一键触达的按钮。这不仅仅是把外卖 App 的图标放在 Dock 栏,而是深度集成,实现从“唤起想法”到“完成支付”的最短路径。今天,我就来详细拆解这个项目的构思、实现过程以及背后的技术选型思考,希望能给同样想提升生活效率,或者对 macOS 开发感兴趣的朋友一些启发。

2. OpenClaw 的核心设计哲学:极简与自动化

在动手写第一行代码之前,我花了大量时间思考 OpenClaw 应该是什么,以及不应该是什么。我不想做一个功能臃肿的“外卖聚合平台”,那违背了初衷。它的设计哲学必须围绕两个词:极简自动化

2.1 为什么是菜单栏(Menu Bar)?

macOS 的菜单栏是一个被严重低估的效率入口。它常驻屏幕顶端,全局可访问,不占用宝贵的屏幕工作区域。对比 Dock 栏、桌面快捷方式甚至 Alfred/LaunchBar 这类启动器,菜单栏有几个独特优势:

  • 零认知负担:图标永远在那里,无需记忆快捷键或打开其他应用。
  • 操作路径最短:鼠标移动到顶部点击即可,比切换到其他应用再操作更直接。
  • 状态可视化:我们可以让图标显示一些状态,比如店铺是否营业、是否有优惠券可用,实现“一眼知天下”。

因此,将核心功能锚定在菜单栏,是缩短用户操作路径、实现“所想即所得”的最佳载体。

2.2 功能边界定义:少即是多

明确了载体,接下来要严格定义功能边界。OpenClaw 的核心用户场景只有一个:快速复购。用户不是来探索新店、写长篇评价的,他们目的明确——以最快速度吃到上次觉得不错的小龙虾。

基于此,我划定了核心功能清单:

  1. 预设菜单管理:允许用户预设1-3个最常点的菜品组合(如“麻辣小龙虾大份+拌面”),并关联到常去的店铺。
  2. 一键下单:点击菜单栏图标,选择预设菜单,应用自动跳转到对应外卖平台(如美团、饿了么)的该店铺该商品页面,并自动填充收货地址、优惠券(如可用),用户只需完成最后的支付确认。
  3. 状态显示:图标可简单显示店铺状态(如打烊时变灰)或优惠信息(如红点提示)。
  4. 订单追踪(进阶):下单后,在菜单栏下拉项中显示订单的简易状态(如“商家已接单”、“骑手已取货”)。

同时,我坚决砍掉了以下功能:

  • 搜索比价:这是外卖 App 的本职工作。
  • 浏览完整菜单:预设就是为了跳过这一步。
  • 多平台比价:增加复杂度,违背“一键”原则。用户通常已对某个平台有偏好和会员权益。

这个“做减法”的过程至关重要,它确保了应用的核心体验聚焦且锋利。

3. 技术架构选型与核心实现路径

要实现上述功能,技术选型是关键。作为一个独立开发者,我需要权衡开发效率、性能、与 macOS 系统的集成度以及后期维护成本。

3.1 原生 vs. 跨平台:为什么选择 Swift 和 AppKit?

市面上有很多优秀的跨平台框架,如 Electron、Flutter、Tauri 等,它们能让我用熟悉的 Web 或 Dart 技术快速构建界面。但对于 OpenClaw 这样一个系统集成度要求高、追求极致性能和原生体验的菜单栏应用,我最终选择了苹果官方的Swift + AppKit组合。

理由如下:

  • 系统级集成能力:AppKit 对菜单栏(NSStatusItem)的支持是原生且最稳定的。可以轻松创建、管理状态栏图标和菜单,响应各种系统事件(如暗色模式切换),这是跨平台框架通过桥接实现所难以比拟的稳定性和性能。
  • 内存与性能:一个常驻内存的菜单栏应用必须轻量。Electron 应用动辄占用上百 MB 内存,而一个精心编写的原生 Swift 应用可以控制在 20MB 以内,对系统资源更友好。
  • 自动化操作的实现:与外卖平台的交互,核心是模拟用户点击和跳转。这涉及到 URL Scheme 调用、WebView 内自动化操作(如自动点击“结算”按钮)。原生WebKit框架与 Swift 的配合更紧密,执行此类自动化脚本(通过 JavaScript)更高效可靠。
  • 分发与沙盒:通过 Mac App Store 分发或直接打包为.app都更方便,沙盒机制也更清晰。

当然,代价是学习曲线和开发速度。但对于一个目标明确、功能聚焦的小工具,投入时间掌握原生开发是值得的,它能带来最好的最终用户体验。

3.2 核心模块拆解

OpenClaw 的架构可以拆解为以下几个核心模块:

1. 状态栏控制器(StatusBarController)这是应用的门面。使用NSStatusItem创建一个状态栏项目,为其设置图标(.template模式以支持暗色主题)。我们需要监听图标的点击事件,弹出NSMenu。菜单项包括预设的菜品、设置入口和退出选项。

import Cocoa class StatusBarController { private var statusItem: NSStatusItem! private let menu = NSMenu() init() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem.button { button.image = NSImage(named: "claw_icon") // 你的图标 button.image?.isTemplate = true // 支持暗色模式 } setupMenu() } private func setupMenu() { // 从持久化存储中加载预设菜单 let presetMenus = PresetManager.shared.loadPresets() for menu in presetMenus { let item = NSMenuItem(title: menu.displayName, action: #selector(placeOrder(_:)), keyEquivalent: "") item.target = self item.representedObject = menu // 关联数据 self.menu.addItem(item) } self.menu.addItem(NSMenuItem.separator()) self.menu.addItem(NSMenuItem(title: "设置...", action: #selector(openSettings(_:)), keyEquivalent: ",")) self.menu.addItem(NSMenuItem.separator()) self.menu.addItem(NSMenuItem(title: "退出 OpenClaw", action: #selector(quitApp(_:)), keyEquivalent: "q")) statusItem.menu = menu } @objc func placeOrder(_ sender: NSMenuItem) { guard let preset = sender.representedObject as? PresetMenu else { return } OrderAutomator.shared.executeOrder(for: preset) } // ... 其他方法 }

2. 预设管理器(PresetManager)负责菜品预设的增删改查和持久化。数据模型(PresetMenu)需要包含:预设名称、关联的外卖平台(美团/饿了么)、店铺ID、商品ID、规格选项ID、用户备注等。持久化使用UserDefaults或更结构化的Core Data/SQLite即可。

3. 订单自动化执行器(OrderAutomator)这是技术核心,也是最复杂的一部分。它的任务是根据一个PresetMenu对象,自动完成从打开平台到跳转至结算页的全过程。这里没有官方API,只能通过模拟用户操作实现。

实现路径通常有两种:

  • URL Scheme 深度链接:理想情况。如果外卖平台提供了完善的 URL Scheme,能直接定位到店铺、商品甚至规格选择页面,那将是最优雅的方案。我们需要仔细研究平台 App 的 URL Scheme 规则。
  • WebView 自动化:更通用的方案。在后台启动一个不可见的WKWebView,加载外卖平台的 H5 页面,然后通过注入并执行 JavaScript 代码,模拟点击“店铺”、“加入购物车”、“选规格”、“去结算”等一系列操作。
import WebKit class OrderAutomator: NSObject, WKNavigationDelegate { static let shared = OrderAutomator() private var webView: WKWebView! private var currentPreset: PresetMenu? private var completion: ((Bool, Error?) -> Void)? private override init() { super.init() let config = WKWebViewConfiguration() // 可配置User-Agent,使其更像移动端浏览器,避免被重定向到PC站 config.applicationNameForUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15" webView = WKWebView(frame: .zero, configuration: config) webView.navigationDelegate = self // 可选:注入通用脚本,如跳过登录弹窗(如果已登录) } func executeOrder(for preset: PresetMenu, completion: ((Bool, Error?) -> Void)? = nil) { self.currentPreset = preset self.completion = completion // 1. 构建目标URL(例如店铺主页) guard let url = URL(string: preset.shopDeepLink) else { return } let request = URLRequest(url: url) // 2. 加载页面 webView.load(request) // 后续步骤在 navigationDelegate 回调中通过JS脚本逐步进行 } // WKNavigationDelegate 方法中,在页面加载完成的回调里执行自动化脚本 func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { // 根据 currentPreset 的信息,分步执行JS // 例如:找到商品元素并点击 let jsClickProduct = """ document.querySelector('[data-product-id="\(currentPreset!.productId)"]').click(); """ webView.evaluateJavaScript(jsClickProduct) { _, error in if let error = error { self.completion?(false, error) return } // 等待一下,然后执行下一步,如选择规格 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.selectSpecification() } } } private func selectSpecification() { // 类似的JS脚本选择规格、点击去结算... // 这是一个简化的示意,实际脚本非常复杂且需要针对不同平台适配 } }

注意:Web 自动化方案极其脆弱。外卖平台的 H5 页面结构随时可能变更,导致你的 CSS 选择器或 JS 路径失效。因此,这个模块必须有完善的错误处理和日志记录,并且需要设计一个“学习模式”,让用户在第一次设置预设时,手动走一遍流程,应用记录下关键节点的点击路径,这比硬编码选择器要稳健得多。

4. 配置界面(PreferencePane)一个简单的视图,用于添加、编辑、删除预设菜单。可以使用SwiftUI来快速构建,并通过AppKitNSHostingController嵌入到原生窗口中。界面元素包括文本框(店铺/菜品名称)、下拉菜单(选择平台)、以及一个“录制宏”按钮来辅助完成上述的自动化路径学习。

4. 开发中的关键挑战与避坑指南

实际开发过程远比设计复杂,以下是几个我踩过的“坑”和解决方案。

4.1 沙盒(Sandbox)权限与网络请求

如果你计划上架 Mac App Store,应用必须在沙盒内运行。这会给自动化操作带来巨大挑战:

  • 网络访问:需要声明com.apple.security.network.client权限。
  • 自动化其他 App:沙盒应用严格禁止控制其他应用。这意味着你无法通过AppleScriptSystem Events去直接操控美团/饿了么的客户端。这是迫使你选择 WebView 自动化方案的核心原因之一,因为操作发生在应用自身的 WebView 内,不违反沙盒规则。
  • 文件访问:存储预设配置需要用户明确授权或存储在容器内。

建议:开发初期可以先关闭沙盒进行功能验证,待核心流程跑通后,再开启沙盒并逐一解决权限问题。这能避免过早被沙盒限制搞得寸步难行。

4.2 对抗反自动化与页面动态加载

外卖平台的页面为了安全和防止爬虫,会采用各种反自动化措施:

  • 元素延迟加载:商品列表可能通过滚动动态加载。你的脚本需要在点击前等待元素出现。使用WKNavigationDelegate的完成回调并不够,还需要在 JS 中使用MutationObserver或轮询检查目标元素是否存在。
  • 验证码:这是自动化之敌。如果触发验证码,整个流程将失败。因此,OpenClaw 的定位必须是“辅助工具”,最终支付环节必须由用户手动完成,这也能避免法律和安全风险。我们的自动化只负责把用户带到“确认支付”页面之前。
  • 随机化的 CSS 类名:一些现代前端框架会生成随机的类名。不能依赖.class-name这样的选择器。更可靠的方式是使用>
← 返回列表