让目标真正成为"MAS 专用"
在第 1 篇中,我们注册了 MAS 专用 Bundle ID 并复制了 FocusTimer MAS 构建目标。但该目标目前仍只是直接分发目标的副本。
MAS 构建必须与直接分发构建在三个方面有所不同:
- 权限 (entitlements) — 仅保留适合 App Store 的最小权限
- Info.plist — 移除 Sparkle 键,添加 App Store 元数据
- 代码 — 分支处理,确保没有 Sparkle 时也能编译
本文将逐一处理这三点。
与第 1 篇相同,
FocusTimer、com.example.FocusTimer.mas等均为示例值。
第 1 步 — MAS 专用 entitlements 文件
entitlements 是列出应用请求哪些系统权限的文件。直接分发构建中,Sparkle 为了安装更新需要额外权限;但 MAS 构建没有 Sparkle,因此那些权限是不必要的。
在项目根目录创建新文件 FocusTimer-MAS.entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
与直接分发用 FocusTimer.entitlements 相比,差异如下:
- 无 Sparkle 相关权限 — 直接分发构建中的
temporary-exception.mach-lookup.*(Sparkle 与安装助手通信所需的例外)在 MAS 构建中没有存在的理由。 - 无
network.client— 移除 Sparkle 后,FocusTimer 本体不进行任何网络调用。不使用的权限理应移除,审核中多余权限也可能被指出。应用请求的权限面越小越好。
上述示例是仅拥有"读写用户主动选择的文件"权限的最简形式。若你的应用实际使用了网络或其他资源,请相应添加对应权限,但绝对不要包含不使用的权限。
第 2 步 — MAS 专用 Info.plist 文件
同样,在项目根目录创建新文件 FocusTimer-MAS-Info.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>
与直接分发用 FocusTimer-Info.plist 相比,有两点不同:
① 去掉所有 Sparkle 的 SU* 键
直接分发用 Info.plist 中包含 SUFeedURL、SUPublicEDKey 等 Sparkle 配置键。MAS 用 Info.plist 中一个都不应有。App Store 审核禁止自带自动更新机制,即使 plist 中仅残留暗示此功能的键也可能成为问题。不包含这些键才是安全的做法。
② 添加 App Store 元数据键
LSApplicationCategoryType— 应用在 App Store 中所属的分类。上述示例中的public.app-category.productivity表示"效率"分类。该值必须与第 3 篇中在 App Store Connect 设置的分类一致,否则审核阶段会出现不匹配警告。ITSAppUsesNonExemptEncryption— 应用是否使用受出口管制的加密技术。若仅使用标准加密(如 HTTPS、标准系统 API)或不使用加密,可设为false。提前硬编码此键,可自动通过每次上传构建时出现的加密调查问卷。
将
ITSAppUsesNonExemptEncryption设为false之前,请确认你的应用确实未使用非标准加密。若不确定,建议参考 Apple 的加密相关文档。
第 3 步 — MAS 目标构建设置
这是第 1 篇 2-3 节中推迟的构建设置更新。在 TARGETS → FocusTimer MAS → Build Settings 中对齐以下设置:
| 构建设置键 | 值 |
|---|---|
PRODUCT_BUNDLE_IDENTIFIER | com.example.FocusTimer.mas |
INFOPLIST_FILE | FocusTimer-MAS-Info.plist |
CODE_SIGN_ENTITLEMENTS | FocusTimer-MAS.entitlements |
ENABLE_APP_SANDBOX | YES |
ENABLE_HARDENED_RUNTIME | YES |
ENABLE_USER_SELECTED_FILES | readwrite |
CODE_SIGN_STYLE | Automatic |
MARKETING_VERSION、CURRENT_PROJECT_VERSION | 与直接分发目标保持一致 |
核心要点如下:
INFOPLIST_FILE和CODE_SIGN_ENTITLEMENTS必须指向刚才创建的 MAS 专用文件。常见的错误是将这两行保持为直接分发用文件不变。- 将
CODE_SIGN_STYLE设为Automatic,Xcode 会自动发放和匹配 Mac App Store 配置文件和 Apple Distribution 证书,无需手动管理签名。 - 若应用不使用网络,则不设置
ENABLE_OUTGOING_NETWORK_CONNECTIONS,这与在 entitlements 中移除network.client的逻辑一致。 - 版本号(
MARKETING_VERSION等)保持与直接分发目标相同的值,以免两个渠道的版本号出现差异。
第 4 步 — Sparkle 代码分支(#if canImport(Sparkle))
即使拆分了配置文件,还有一个问题未解决:源代码中某处有 import Sparkle 及使用 Sparkle 的代码,而 MAS 目标中未链接 Sparkle,因此 import 语句本身会产生编译错误。
解决方案是用 Swift 的条件编译指令 #if canImport(Sparkle) 包裹 Sparkle 相关代码。
为何用 canImport — 不使用自定义标志的原因
也可以用 #if MAS_BUILD 等自定义编译标志来分支。但这样就需要人工同步"MAS 目标开启该标志,直接分发目标关闭该标志"的设置。随着目标增多或设置变化,容易出现不一致。
canImport(Sparkle) 则不同。它由编译器直接检查"Sparkle.framework 是否链接到此目标"。由于第 1 篇中已从 MAS 目标的包依赖中删除了 Sparkle,在 MAS 目标中 canImport(Sparkle) 会自动为假 (false),无需单独同步任何标志。这就是 macOS 应用分离 MAS 渠道时将此模式作为事实标准的原因。
分支 ① — 完全包裹仅含 Sparkle 代码的文件
仅包含自动更新逻辑的文件(如 UpdaterCoordinator.swift)整体用 #if 包裹:
#if canImport(Sparkle)
import Foundation
import Sparkle
import Combine
@Observable
@MainActor
final class UpdaterCoordinator {
// 包裹 Sparkle 更新器的代码 …
}
#endif
在 MAS 构建中,此文件会像空文件一样编译,不产生任何影响。
分支 ② — 同时包裹使用 Sparkle 的代码
创建 UpdaterCoordinator 并将其传递给界面的代码也需要分支。否则 MAS 构建会引用"不存在的类型"。
@main
struct FocusTimerApp: App {
@State private var viewModel = CompositionRoot.makeContentViewModel()
#if canImport(Sparkle)
@State private var updater = UpdaterCoordinator()
#endif
var body: some Scene {
Settings {
#if canImport(Sparkle)
PreferenceView(viewModel: viewModel, updater: updater)
#else
PreferenceView(viewModel: viewModel)
#endif
}
MenuBarExtra {
#if canImport(Sparkle)
MenuBarContent(updater: updater)
#else
MenuBarContent()
#endif
} label: {
// 菜单栏图标 …
}
}
}
private struct MenuBarContent: View {
#if canImport(Sparkle)
@Bindable var updater: UpdaterCoordinator
#endif
var body: some View {
Button("显示 FocusTimer") { /* … */ }
#if canImport(Sparkle)
Button("检查更新…") { updater.checkForUpdates() }
.disabled(!updater.canCheckForUpdates)
#endif
// 其他共用菜单项 …
}
}
要点有三:
updater字段声明本身用#if包裹。- 接收
updater的界面(PreferenceView、MenuBarContent)分为有 Sparkle 和无 Sparkle 两种形式。 - “检查更新…“等 Sparkle 专用 UI 元素也用
#if包裹。在 MAS 构建中,这些菜单项不会出现——App Store 版不应有自带更新菜单,这是正确的。
设置界面(PreferenceView)中的"启用自动更新"开关等元素也都以相同的模式包裹即可。
第 2 篇小结
跟到这里,你现在已经准备好以下内容:
- ✅ MAS 专用权限文件
FocusTimer-MAS.entitlements(最小权限) - ✅ MAS 专用
FocusTimer-MAS-Info.plist(移除 Sparkle 键,添加 App Store 元数据) - ✅ MAS 目标构建设置已对齐
- ✅ 用
#if canImport(Sparkle)完成了自动更新代码的分支
现在,FocusTimer MAS 目标已真正有别于直接分发目标,成为可以上架 App Store 的形态。剩下的工作是搭建将此构建实际上传到 App Store Connect 的路径。
下一篇将创建用于上传的 ExportOptions-MAS.plist、在 App Store Connect 中注册应用记录,并介绍如何验证两个渠道在未来不会被破坏,从而收尾本系列。