Shipping to the App Store
Signing, TestFlight, privacy manifests and App Review — the last mile, mapped so it holds no surprises.
By the end you will be able to
- Explain what code signing proves and what Xcode automates about it
- Run a TestFlight programme that actually catches problems
- Avoid the review rejections that account for most first-submission failures
Your app works perfectly in the simulator and on your own device. What is the most common reason a first App Store submission gets rejected anyway?
Signing: what the ceremony proves
Every app on a device carries a cryptographic chain answering two questions: who built this (your Developer Program identity) and is it allowed to run here (a provisioning profile binding app ID + capabilities + devices/distribution).
You need the mental model more than the manual steps, because "Automatically manage signing" handles the mechanics and is the right choice until a CI pipeline or an enterprise setup forces otherwise:
- Certificate — your signing identity (development vs distribution).
- App ID — the bundle identifier plus its capabilities (push, iCloud, HealthKit…). Capabilities are declared here and in entitlements; mismatches are the classic "works locally, App Store build crashes on the capability" bug.
- Provisioning profile — ties the two together with a scope: your devices (development), TestFlight/App Store (distribution).
When signing breaks — and on every team it someday does — the fix is nearly always: let Xcode regenerate (Signing & Capabilities ▸ re-tick automatic), not hand-surgery on profiles. The hand-surgery world is an afternoon matching certificate, profile and entitlement triples by eye — a fix that holds until the next capability change desynchronises them again — versus one re-tick rebuilding all three consistently.
Versioning and the archive path
Two numbers with different jobs:
- Marketing version (
1.4.2) — what users see; you choose its meaning. - Build number (
87) — must be unique per upload; TestFlight and App Store Connect key on it. Teams auto-increment it in CI precisely because "upload rejected: build already exists" at 6pm on release day is a rite of passage nobody needs twice.
The path to users is always: Product ▸ Archive → Organizer → Distribute App → App Store Connect. The archive step matters because it builds the release configuration — optimisations on, debug hooks off — which is also why "it only crashes in TestFlight" bugs exist: you have been testing a different binary all sprint. Archive early, not on submission day.
Privacy: manifests and the nutrition label
Since 2024 this is where careless submissions die. Three artefacts must agree:
- Privacy manifest (
PrivacyInfo.xcprivacy) — declares data collected and, critically, required-reason APIs: innocuous-looking calls (UserDefaults, file timestamps, boot time, disk space) that have been abused for fingerprinting, and now require a declared reason code. Third-party SDKs ship their own manifests; your submission is judged on the aggregate. - Purpose strings in Info.plist — the text in permission dialogs.
NSCameraUsageDescription: "Scan documents to attach them to notes"passes;"This app needs camera access"is a rejection template. Say what the user gets. - The App Store privacy label — the questionnaire in App Store Connect. It must match what the binary and its SDKs actually do; analytics SDKs quietly collecting device data that your label denies is a rejection and a trust problem.
The engineering habit that keeps all three honest: know your SDKs. Every dependency you add is privacy surface you now vouch for.
TestFlight: your rehearsal space
Two tiers, and the difference is review:
| Internal | External | |
|---|---|---|
| Who | up to 100 App Store Connect users | up to 10,000 via email/public link |
| Review | none — builds available in minutes | first build of a version needs a (lighter) review |
| Use for | the team, daily builds | the real rehearsal audience |
Builds expire after 90 days; testers get your "What to Test" notes — write real ones ("try checkout with a saved card; we changed the flow") because directed testers find directed bugs. Crashes and screenshots-with-feedback flow back into App Store Connect.
The under-used play: external TestFlight is a dress rehearsal for review itself. The first external build passes through a beta review that catches many of the same 2.1-style issues — broken links, permission strings — weeks before your real submission, when fixing them is calm work instead of a launch delay.
App Review: the checklist the stranger carries
The rejections that recur, and their prevention:
- 2.1 Completeness — every feature reachable, no placeholders, links live, and a working demo account in the review notes if anything sits behind login. The single most preventable failure.
- 2.3 Metadata — screenshots showing the actual app, description matching the binary, no other platforms named.
- 4.2 Minimum functionality — the app must do something an app should do; thin website wrappers fail here.
- 5.1.1 Data collection — ask permission in context, explain purpose, and never gate the app on permissions it doesn't need.
- 4.8 Login services — if you offer third-party or social login, you must also offer a privacy-preserving option: one that limits data to name and email, lets users hide the email, and does no ad tracking without consent. Sign in with Apple is the usual way to satisfy this — but since the 2024 revision it is one way, not a mandate.
- 3.1 Payments — in-app purchases of digital goods go through In-App Purchase; physical goods and services must not. (Know the big 2025 carve-out: on the US storefront, apps may additionally link out to external web checkout for digital goods.) Misclassifying is both a rejection and, in the worst case, a business-model conversation you should have had earlier.
Review typically returns in a day or two. When rejected: fix, or appeal politely with facts — reviewers are humans on quota, and a clear explanation of what they may have missed regularly succeeds. When approved, you choose release timing — and phased release (7-day gradual rollout, from 1% up) plus the ability to halt it is the free safety net every 1.0 should use.
Build numbers must increase per upload. This model of App Store Connect's gate shows why CI owns the counter — what does it print?
struct UploadGate {
var acceptedBuilds: [Int] = []
mutating func submit(build: Int) -> String {
if let last = acceptedBuilds.last, build <= last {
return "REJECTED build \(build) — must exceed \(last)"
}
acceptedBuilds.append(build)
return "accepted build \(build)"
}
}
var gate = UploadGate()
// Monday: CI uploads
print(gate.submit(build: 41))
// Wednesday: hotfix branch, developer uploads manually from an older checkout
print(gate.submit(build: 41))
// The panic fix:
print(gate.submit(build: 42))
// Friday: main branch CI, counter never knew about 42
print(gate.submit(build: 42))Your notes app adds a premium tier: unlimited notes plus a paid paper notebook mailed quarterly. How must you charge?
Automate the pre-flight checklist
Encode the submission checklist as running code: readyToSubmit should collect every failure, not stop at the first — a reviewer won't stop at one either.
Phase the rollout
Model phased release: given daily percentages and a crash threshold, roll forward day by day but halt the moment crashes exceed the threshold.
Write the release plan for this learning platform if it were an iOS app: the TestFlight tiers and who is in each, the "What to Test" note for the first external build, the reviewer notes (what needs explaining? any permissions?), and your phased-release halt criterion as a number. One page, the way you would actually post it for a team.
You can now:
- Explain the signing chain and let Xcode do the mechanics
- Run internal and external TestFlight as rehearsal, not formality
- Keep manifests, purpose strings and the privacy label agreeing
- Pre-empt the 2.1/3.1/5.1 rejections and ship behind a phased release
This completes the platform-craft arc: architecture made it testable, tests made it trustworthy, accessibility made it usable, and this lesson gets it into users' hands.