Another Distribution Channel — the Mac App Store
The previous series covered the one-time setup for distributing a macOS app directly with a Developer ID. Once you have certificates, notarization, Sparkle automatic updates, and a hosted update feed in place, you can let users download a .dmg file directly without going through the App Store.
This series covers the one-time setup for putting that same app on the Mac App Store (MAS) as well. The two distribution methods are not an either/or choice. You can run a single app through both the direct distribution channel and the App Store channel at the same time. The App Store has Apple handle payments, refunds, and search visibility on your behalf, and it carries higher user trust, so running it alongside direct distribution is a common choice.
This series assumes your app is already being distributed directly with a Developer ID and already has Sparkle automatic updates built in. That setup was covered starting from Part 1 of the “Distributing a macOS App Yourself” series. If you haven’t read it yet, I recommend going through it first.
Why MAS Needs “Another Target”
A directly distributed app includes Sparkle automatic updates — the feature that lets the app check an update feed on its own, download a new version, and replace itself.
But the Mac App Store review rules forbid this behavior. An app on the App Store must not download code from outside to update itself; updates must happen only through the App Store. In other words, a build destined for MAS must not contain Sparkle.
The direct distribution build, however, must have Sparkle. A single build artifact cannot satisfy both requirements at once. So the solution is this:
One codebase, two build targets.
| Channel | Build target | Sparkle | Distributed via |
|---|---|---|---|
| Developer ID | FocusTimer | Included | Direct distribution (.dmg) |
| Mac App Store | FocusTimer MAS | Excluded | App Store |
You share a single source codebase, but split it into two build targets, linking Sparkle in only one of them. This series covers the one-time setup for creating that second target (FocusTimer MAS).
What This Series Builds
Over three parts, you will put the following in place.
- (Part 1, this post) Registering a MAS-only Bundle ID + duplicating the Xcode build target
- (Part 2) The configuration files (entitlements and Info.plist) and code branching that separate the two channels
- (Part 3) An
ExportOptions-MAS.plistfor upload + App Store Connect registration + build verification
By the end of the series, you should have:
- A MAS-only Bundle ID registered in the Apple Developer Portal
- A
FocusTimer MASbuild target with the Sparkle dependency removed - A MAS-only entitlements file and Info.plist file
- Code branched with
#if canImport(Sparkle) - An
ExportOptions-MAS.plistfor App Store upload
The Example App — FocusTimer
As in the previous series, we’ll use a fictional macOS app, FocusTimer (a simple timer app for managing focus sessions), as the example. FocusTimer, the bundle identifier com.example.FocusTimer, the Team ID ABCDE12345, and so on are all example values. In practice, swap them for your own app and account information.
This post was written for Xcode 26.
Step 1 — Register a MAS-only Bundle ID
Why a Separate Bundle ID
If the direct distribution channel’s Bundle ID is com.example.FocusTimer, the MAS channel uses a different Bundle ID.
| Channel | Bundle ID |
|---|---|
| Developer ID | com.example.FocusTimer |
| Mac App Store | com.example.FocusTimer.mas |
You append .mas to the existing Bundle ID. Why bother splitting them?
- Both channels can coexist on one Mac — If the Bundle IDs are the same, macOS treats the two apps as the same app and they conflict. If they differ, you can install the directly distributed build and the App Store build on the same machine at the same time (handy for development and testing).
- Separate
UserDefaultsdomains — Settings storage is partitioned by Bundle ID, so the two channels’ settings don’t get mixed up. - Avoiding Launch Services conflicts — It prevents confusion when macOS decides which app to send a request like “open this app” to.
Keep the direct distribution channel’s Bundle ID unchanged. It’s the identifier for your existing users, so it must not be altered. You’re registering one additional new ID for MAS.
Registration Procedure
- Sign in at developer.apple.com/account (with your Apple Developer Program account)
- Certificates, Identifiers & Profiles → Identifiers in the left menu → the
+button - Select App IDs → Continue → choose type App → Continue
- Description: Enter an easily recognizable name (e.g.,
FocusTimer MAS) - Bundle ID:
- Select Explicit
- Enter:
com.example.FocusTimer.mas
- Capabilities: Turn off every feature the app doesn’t use. If you don’t use iCloud, push notifications, in-app purchases, Sign in with Apple, and so on, you don’t need to enable anything. (App Sandbox is handled by entitlements on the Xcode side, so there’s no need to enable it here.)
- Continue → Register
Verification
If the Identifiers list shows an entry for FocusTimer MAS — com.example.FocusTimer.mas, registration is complete.
Step 2 — Duplicate the Xcode Build Target
Now you’ll create a FocusTimer MAS build target in the Xcode project. The fastest way is to duplicate the existing target.
2-1. Duplicate the Target
- Open
FocusTimer.xcodeprojin Xcode - Click the project icon (blue) at the top of the Project navigator (left panel)
- In the TARGETS list in the center editing area, right-click
FocusTimer→ Duplicate - When the dialog appears, select Duplicate Only
- The new target is created as
FocusTimer copy. Double-click the name and change it toFocusTimer MAS - If the prompt “Add a new Scheme?” appears, choose Activate (or add it later in Manage Schemes)
2-2. Change the Bundle ID Immediately
The very first thing to do right after duplicating is to swap the Bundle ID. If you leave it as is, both targets will have the same ID.
Change TARGETS → FocusTimer MAS → General → Identity → Bundle Identifier to the value you registered in Step 1.
com.example.FocusTimer.mas
2-3. A Key Pitfall — the “Cleanup Burden” Duplicate Leaves Behind
This is the trickiest part of this post. Xcode’s Duplicate Target operates with “safe defaults,” but that very safety leaves behind cleanup work that requires manual attention. Right after duplicating, you need to check the following four things.
① Make the new target include the source files
For safety, Duplicate automatically excludes all source files from the new target. Left as is, the FocusTimer MAS target is an empty shell — building it produces a build with no code in it. You need to fix up the target memberships so the new target includes the existing source files again.
② Remove the Sparkle dependency from the MAS target
Duplicate copies the original’s Swift Package dependencies verbatim. As a result, Sparkle tags along into the FocusTimer MAS target too. The whole starting point of this series was “the MAS build must not have Sparkle,” so remove Sparkle from the MAS target’s package dependency list and from the Frameworks build phase.
③ Clean up the temporary Info.plist file
Duplicate creates a temporary Info.plist file like FocusTimer copy-Info.plist. Since we’ll create a dedicated MAS Info.plist separately in Part 2, delete these temporary files — both the project reference and the actual file.
④ Update the MAS target’s build settings
You need to align build settings such as the bundle identifier, the Info.plist path, and the entitlements path for MAS. We’ll handle this all at once in Part 2, after creating the dedicated configuration files.
Items ① and ② can mostly be handled from the target membership and package dependency screens in the Xcode UI, but if duplicate references left behind by the duplication process don’t get cleaned up neatly, you may sometimes need to look directly at the project file (
.pbxproj). Once you think the cleanup is done, the most reliable way to verify is to build both targets separately and check whetherimport Sparklebreaks in the MAS target build (Part 2 covers this code branching).
Part 1 Recap
If you’ve followed along this far, you now have:
- ✅ A MAS-only Bundle ID (
com.example.FocusTimer.mas) registered in the Apple Developer Portal - ✅ A
FocusTimer MASbuild target created by duplication - ✅ An understanding of the cleanup burden duplication leaves behind (source membership, Sparkle dependency, temporary files)
The “container” — the target — has been created. But this target is still essentially just a copy of FocusTimer; it hasn’t truly become “MAS-specific” yet. A MAS build needs to use different entitlements and a different Info.plist than the direct distribution build, and the code also has to branch so it doesn’t break when Sparkle is absent.
In the next part, we’ll cover the configuration files and code branching that separate the two channels.