另一个分发渠道 — Mac App Store
前一系列介绍了用 Developer ID 直接分发 macOS 应用的一次性准备工作。配齐证书、公证、Sparkle 自动更新和更新 Feed 托管后,用户无需经过 App Store 即可直接下载 .dmg 文件。
本系列介绍将同一应用上架 Mac App Store (MAS) 的一次性准备工作。两种分发方式并非二选一,可以同时运营直接分发渠道和 App Store 渠道。App Store 由 Apple 代劳处理支付、退款和搜索曝光,用户信任度也更高,因此与直接分发并行运营的情况很常见。
本系列以应用已通过 Developer ID 直接分发、且已集成 Sparkle 自动更新为前提。该配置从 Developer ID 直接分发系列第 1 篇开始介绍。若尚未阅读,建议先行查阅。
为何 MAS 需要"另一个目标"
直接分发的应用包含 Sparkle 自动更新——应用自行检查更新 Feed,下载新版本并替换自身的功能。
然而,Mac App Store 的审核规定禁止此类行为。上架 App Store 的应用不得从外部下载代码更新自身,更新必须且只能通过 App Store 进行。也就是说,提交 MAS 的构建中不能包含 Sparkle。
但直接分发构建必须包含 Sparkle。同一构建产物无法同时满足两种要求。因此,解决方案是:
同一代码库,两个构建目标。
| 渠道 | 构建目标 | Sparkle | 分发途径 |
|---|---|---|---|
| Developer ID | FocusTimer | 包含 | 直接分发(.dmg) |
| Mac App Store | FocusTimer MAS | 排除 | App Store |
共享同一份源代码,将构建目标分为两个,只在其中一个中链接 Sparkle。本系列介绍创建第二个目标(FocusTimer MAS)的一次性准备工作。
本系列将完成的内容
共三篇,逐步搭建以下内容:
- (第 1 篇,本文) 注册 MAS 专用 Bundle ID + 复制 Xcode 构建目标
- (第 2 篇) 区分两个渠道的配置文件(entitlements 和 Info.plist)与代码分支
- (第 3 篇) 用于上传的
ExportOptions-MAS.plist+ App Store Connect 注册 + 构建验证
系列结束时,你应该已经准备好以下内容:
- 在 Apple Developer Portal 注册的 MAS 专用 Bundle ID
- 已移除 Sparkle 依赖的
FocusTimer MAS构建目标 - MAS 专用 entitlements 文件和 Info.plist 文件
- 用
#if canImport(Sparkle)分支的代码 - 用于 App Store 上传的
ExportOptions-MAS.plist
示例应用 — FocusTimer
与前一系列相同,以虚构的 macOS 应用 FocusTimer(管理专注时间的简单计时器应用)为例。FocusTimer、Bundle Identifier com.example.FocusTimer、Team ID ABCDE12345 等均为示例值,实际使用时请替换为你自己的应用和账户信息。
本文基于 Xcode 26 编写。
第 1 步 — 注册 MAS 专用 Bundle ID
为何需要独立的 Bundle ID
若直接分发渠道的 Bundle ID 为 com.example.FocusTimer,则 MAS 渠道使用不同的 Bundle ID。
| 渠道 | Bundle ID |
|---|---|
| Developer ID | com.example.FocusTimer |
| Mac App Store | com.example.FocusTimer.mas |
在原有 Bundle ID 后追加 .mas。为何要分开?
- 同一台 Mac 上两个渠道可以共存 — Bundle ID 相同时,macOS 会将两个应用识别为同一个并产生冲突。若不同,直接分发版和 App Store 版可以同时安装在同一台机器上(便于开发和测试)。
UserDefaults域名隔离 — 设置存储空间以 Bundle ID 为单位划分,两个渠道的设置不会混淆。- 避免 Launch Services 冲突 — 防止 macOS 在处理"打开此应用"等请求时产生的混乱。
请保持直接分发渠道的 Bundle ID 不变。这是识别现有用户的基准,不能更改。MAS 用的是额外注册的一个新 ID。
注册流程
- 登录 developer.apple.com/account(使用 Apple Developer Program 账户)
- Certificates, Identifiers & Profiles → 左侧菜单 Identifiers →
+按钮 - 选择 App IDs → Continue → 类型选择 App → Continue
- Description:输入易于识别的名称(如
FocusTimer MAS) - Bundle ID:
- 选择 Explicit(明确)
- 输入:
com.example.FocusTimer.mas
- Capabilities:关闭应用不使用的所有功能。若不使用 iCloud、推送通知、应用内购买、Sign in with Apple 等,无需开启任何项目。(App Sandbox 由 Xcode 侧的 entitlements 处理,此处无需开启。)
- Continue → Register
验证
若 Identifiers 列表中显示 FocusTimer MAS — com.example.FocusTimer.mas 条目,则注册完成。
第 2 步 — 复制 Xcode 构建目标
现在在 Xcode 项目中创建 FocusTimer MAS 构建目标。最快的方法是复制现有目标。
2-1. 复制 (Duplicate) 目标
- 在 Xcode 中打开
FocusTimer.xcodeproj - 点击 Project navigator(左侧面板)最上方的项目图标(蓝色)
- 在中间编辑区域的 TARGETS 列表中右键点击
FocusTimer→ Duplicate - 弹出对话框时选择 Duplicate Only
- 新目标名称为
FocusTimer copy,双击名称将其改为FocusTimer MAS - 若弹出"是否添加新 Scheme?“提示,选择 Activate(或稍后在 Manage Schemes 中添加)
2-2. 立即修改 Bundle ID
复制后最先要做的是替换 Bundle ID。若保持不变,两个目标将拥有相同的 ID。
将 TARGETS → FocusTimer MAS → General → Identity → Bundle Identifier 改为第 1 步中注册的值:
com.example.FocusTimer.mas
2-3. 关键陷阱 — Duplicate 留下的"清理负担”
这是本篇中最棘手的部分。Xcode 的 Duplicate Target 以**“安全默认值”**运行,但这种安全反而留下了需要手动处理的清理工作。复制完成后需要检查以下四点:
① 让新目标包含源文件
为了安全,Duplicate 会自动将所有源文件从新目标中排除。若保持不变,FocusTimer MAS 目标就是一个空壳,构建时不会包含任何代码。需要重新整理目标成员关系,使新目标重新包含现有源文件。
② 从 MAS 目标中移除 Sparkle 依赖
Duplicate 会原样复制原始目标的 Swift Package 依赖。因此 Sparkle 也会随之进入 FocusTimer MAS 目标。本系列的出发点正是"MAS 构建中不能有 Sparkle",所以需要从 MAS 目标的包依赖列表和 Frameworks 构建阶段中删除 Sparkle。
③ 清理临时 Info.plist 文件
Duplicate 会创建类似 FocusTimer copy-Info.plist 的临时 Info.plist 文件。由于我们将在第 2 篇中单独创建 MAS 专用的 Info.plist,这些临时文件(包括项目引用和实际文件)应全部删除。
④ 更新 MAS 目标的构建设置
Bundle Identifier、Info.plist 路径、entitlements 路径等构建设置需要调整为 MAS 专用。此工作将在第 2 篇创建专用配置文件后统一处理。
①·② 大多可通过 Xcode UI 中的目标成员关系和包依赖界面处理,但若复制过程留下的重复引用无法彻底清除,有时需要直接查看项目文件(
.pbxproj)。清理完成后,分别构建两个目标,确认 MAS 目标构建中import Sparkle是否报错,是最可靠的验证方式(第 2 篇中介绍此代码分支)。
第 1 篇小结
跟到这里,你现在已经准备好以下内容:
- ✅ 在 Apple Developer Portal 注册了 MAS 专用 Bundle ID(
com.example.FocusTimer.mas) - ✅ 通过复制创建了
FocusTimer MAS构建目标 - ✅ 了解了复制留下的清理负担(源文件成员关系、Sparkle 依赖、临时文件)
目标这个"容器"已经创建好了。但这个目标目前本质上还只是 FocusTimer 的副本,并未真正成为"MAS 专用"。MAS 构建需要使用与直接分发构建不同的权限 (entitlements) 和不同的 Info.plist,代码也需要分支以便在没有 Sparkle 时不报错。
下一篇将介绍区分两个渠道的配置文件与代码分支。