Making the Target Truly “MAS-Specific”
In Part 1, we registered a MAS-only Bundle ID and duplicated the FocusTimer MAS build target. But that target is still just a copy of the direct distribution target.
A MAS build has to differ from the direct distribution build in three ways.
- Entitlements — only the minimum set appropriate for the App Store
- Info.plist — drop the Sparkle keys, add App Store metadata
- Code — branch so it compiles even without Sparkle
In this post, we’ll split all three.
As in Part 1,
FocusTimer,com.example.FocusTimer.mas, and so on are all example values.
Step 1 — The MAS-only Entitlements File
Entitlements is a file that lists which system permissions an app requests. In the direct distribution build, Sparkle required extra permissions to install updates, but the MAS build has no Sparkle, so those permissions are unnecessary.
Create a new FocusTimer-MAS.entitlements file in the project root.
<?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>
Compared with the direct distribution FocusTimer.entitlements, here’s how it differs.
- No Sparkle-related permissions — The
temporary-exception.mach-lookup.*entries from the direct distribution build (the exception that lets Sparkle communicate with the installer helper) have no reason to be in the MAS build. - No
network.client— With Sparkle removed, the FocusTimer app itself makes no network calls. It’s honest to drop permissions you don’t use, and unnecessary permissions are also flagged during review. The smaller the permission surface an app requests, the better.
The example above is the simplest possible form, with only enough permission to “read and write files the user explicitly picks.” If your app actually uses the network or other resources, add the corresponding permissions, but never include permissions you don’t use.
Step 2 — The MAS-only Info.plist File
Likewise, create a new FocusTimer-MAS-Info.plist in the project root.
<?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>
There are two differences from the direct distribution FocusTimer-Info.plist.
① All the Sparkle SU* keys are dropped
The direct distribution Info.plist contained Sparkle configuration keys like SUFeedURL and SUPublicEDKey. The MAS Info.plist must contain none of these keys. App Store review forbids self-contained automatic updates, so even a key that merely hints at such a feature remaining in the plist can be a problem. It’s safest not to include the key at all.
② App Store metadata keys are added
LSApplicationCategoryType— the category the app belongs to on the App Store. Thepublic.app-category.productivityin the example above means the “Productivity” category. This value must match the category you set in App Store Connect in Part 3, so a mismatch warning doesn’t appear during review.ITSAppUsesNonExemptEncryption— whether the app uses encryption technology subject to export regulations. If you use only standard encryption (e.g., HTTPS, standard system APIs) or no encryption at all, you can set this tofalse. Hardcoding this key in advance automatically clears the encryption questionnaire that appears every time you upload a build.
Before setting
ITSAppUsesNonExemptEncryptiontofalse, verify that your app really doesn’t use non-standard encryption. If you’re unsure, it’s safer to consult Apple’s documentation on encryption.
Step 3 — MAS Target Build Settings
Now for the build settings update we deferred in Part 1, Section 2-3. In TARGETS → FocusTimer MAS → Build Settings, align the following.
| Build setting key | Value |
|---|---|
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 | Same as the direct distribution target |
The key points are these.
INFOPLIST_FILEandCODE_SIGN_ENTITLEMENTSmust point to the MAS-only files you just created. A common mistake is leaving these two lines pointing at the direct distribution files.- Setting
CODE_SIGN_STYLEtoAutomaticlets Xcode automatically issue and match the Mac App Store provisioning profile and the Apple Distribution certificate. You don’t need to manage signing yourself. - If your app doesn’t use the network, don’t set
ENABLE_OUTGOING_NETWORK_CONNECTIONS. This is the same reasoning as droppingnetwork.clientfrom the entitlements. - Keep the version numbers (
MARKETING_VERSION, etc.) at the same values as the direct distribution target, so the two channels’ versions don’t drift apart.
Step 4 — Branching the Sparkle Code (#if canImport(Sparkle))
Even after splitting the configuration files, one problem remains. Somewhere in the source code there’s import Sparkle and code that uses Sparkle, but Sparkle isn’t linked into the MAS target, so the import statement itself produces a compile error.
The solution is to wrap the Sparkle-related code with Swift’s conditional compilation directive #if canImport(Sparkle).
Why canImport — Why Not Use a Separate Flag
You could also branch with a custom compilation flag like #if MAS_BUILD. But then you’d have to manually keep in sync the setting “turn the flag on for the MAS target, off for the direct distribution target.” That’s easy to get out of sync as targets multiply or settings change.
canImport(Sparkle) is different. It has the compiler directly check whether “Sparkle.framework is linked into this target.” Since we removed Sparkle from the MAS target’s package dependencies in Part 1, canImport(Sparkle) is automatically false in the MAS target. There’s no separate flag to keep in sync. That’s why macOS apps use this pattern as a de facto standard when separating out a MAS channel.
Branch ① — Wrap Sparkle-Only Files Entirely
A file that contains only the automatic update logic (e.g., UpdaterCoordinator.swift) gets the entire file wrapped in #if.
#if canImport(Sparkle)
import Foundation
import Sparkle
import Combine
@Observable
@MainActor
final class UpdaterCoordinator {
// Code wrapping the Sparkle updater …
}
#endif
In the MAS build, this file compiles as if it were empty and has no effect at all.
Branch ② — Wrap the Sparkle Users Too
The code that creates UpdaterCoordinator and passes it to the UI also needs to be branched. Otherwise the MAS build would reference a “type that doesn’t exist.”
@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: {
// Menu bar icon …
}
}
}
private struct MenuBarContent: View {
#if canImport(Sparkle)
@Bindable var updater: UpdaterCoordinator
#endif
var body: some View {
Button("Show FocusTimer") { /* … */ }
#if canImport(Sparkle)
Button("Check for Updates…") { updater.checkForUpdates() }
.disabled(!updater.canCheckForUpdates)
#endif
// Other shared menu items …
}
}
There are three key points.
- The
updaterfield declaration itself is wrapped in#if. - Any view that receives
updater(PreferenceView,MenuBarContent) is branched into two forms — one for when Sparkle is present and one for when it isn’t. - Sparkle-only UI elements like “Check for Updates…” are also wrapped in
#if. In the MAS build, this menu item doesn’t appear at all — and rightly so, the App Store edition shouldn’t have a self-update menu.
Elements such as the “Enable automatic updates” toggle in the settings screen (PreferenceView) should all be wrapped with the same pattern.
Part 2 Recap
If you’ve followed along this far, you now have:
- ✅ A MAS-only entitlements file
FocusTimer-MAS.entitlements(minimum permissions) - ✅ A MAS-only
FocusTimer-MAS-Info.plist(Sparkle keys removed, App Store metadata added) - ✅ The MAS target’s build settings squared away
- ✅ Automatic update code branched with
#if canImport(Sparkle)
Now the FocusTimer MAS target is truly different from the direct distribution target — it’s in a shape that can be put on the App Store. What’s left is to set up the path for actually uploading this build to App Store Connect.
In the next part, we’ll wrap up the series by creating an ExportOptions-MAS.plist for upload, registering an app record in App Store Connect, and covering how to verify that the two channels stay unbroken going forward.