另一个分发渠道 — 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 IDFocusTimer包含直接分发(.dmg
Mac App StoreFocusTimer 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 IDcom.example.FocusTimer
Mac App Storecom.example.FocusTimer.mas

在原有 Bundle ID 后追加 .mas。为何要分开?

  • 同一台 Mac 上两个渠道可以共存 — Bundle ID 相同时,macOS 会将两个应用识别为同一个并产生冲突。若不同,直接分发版和 App Store 版可以同时安装在同一台机器上(便于开发和测试)。
  • UserDefaults 域名隔离 — 设置存储空间以 Bundle ID 为单位划分,两个渠道的设置不会混淆。
  • 避免 Launch Services 冲突 — 防止 macOS 在处理"打开此应用"等请求时产生的混乱。

保持直接分发渠道的 Bundle ID 不变。这是识别现有用户的基准,不能更改。MAS 用的是额外注册的一个新 ID

注册流程

  1. 登录 developer.apple.com/account(使用 Apple Developer Program 账户)
  2. Certificates, Identifiers & Profiles → 左侧菜单 Identifiers+ 按钮
  3. 选择 App IDsContinue → 类型选择 AppContinue
  4. Description:输入易于识别的名称(如 FocusTimer MAS
  5. Bundle ID
    • 选择 Explicit(明确)
    • 输入:com.example.FocusTimer.mas
  6. Capabilities:关闭应用不使用的所有功能。若不使用 iCloud、推送通知、应用内购买、Sign in with Apple 等,无需开启任何项目。(App Sandbox 由 Xcode 侧的 entitlements 处理,此处无需开启。)
  7. ContinueRegister

验证

Identifiers 列表中显示 FocusTimer MAS — com.example.FocusTimer.mas 条目,则注册完成。

第 2 步 — 复制 Xcode 构建目标

现在在 Xcode 项目中创建 FocusTimer MAS 构建目标。最快的方法是复制现有目标。

2-1. 复制 (Duplicate) 目标

  1. 在 Xcode 中打开 FocusTimer.xcodeproj
  2. 点击 Project navigator(左侧面板)最上方的项目图标(蓝色)
  3. 在中间编辑区域的 TARGETS 列表中右键点击 FocusTimerDuplicate
  4. 弹出对话框时选择 Duplicate Only
  5. 新目标名称为 FocusTimer copy,双击名称将其改为 FocusTimer MAS
  6. 若弹出"是否添加新 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 时不报错。

下一篇将介绍区分两个渠道的配置文件与代码分支