Tooling and Release
Dependency managers and XCFrameworks, code signing and provisioning, `.xcconfig` and secrets, crash symbolication, linking choices, and shipping safely.
9 questions
JuniorTheoryVery commonIn iOS code signing, what does a certificate, a provisioning profile, an entitlement, and an App ID each prove?
In iOS code signing, what does a certificate, a provisioning profile, an entitlement, and an App ID each prove?
A certificate proves who built it, via your private key. An App ID names the app by bundle id; entitlements declare its capabilities. A provisioning profile ties certificate, App ID, entitlements and devices together, authorizing the build.
Common mistakes
- ✗Confusing the certificate (who signed) with the provisioning profile (what may run where)
- ✗Thinking entitlements are granted by the certificate rather than declared and carried by the profile
- ✗Believing a provisioning profile is unnecessary for App Store distribution
Follow-up questions
- →What is the difference between a development and a distribution provisioning profile?
- →Why must an entitlement also appear in the provisioning profile to actually work?
JuniorTheoryCommonWhat do the dependency managers Swift Package Manager (SPM), CocoaPods, and Carthage each do, and why has SPM become the default?
What do the dependency managers Swift Package Manager (SPM), CocoaPods, and Carthage each do, and why has SPM become the default?
SPM is Apple's built-in, source-based manager inside Xcode, building packages from Git with no extra tooling. CocoaPods is a centralized Ruby tool that rewrites the workspace; Carthage only builds frameworks. SPM won by being native — no Podfile.
Common mistakes
- ✗Thinking SPM cannot consume a binary framework (XCFramework) — it can via a binary target
- ✗Believing CocoaPods and SPM cannot coexist in one project
- ✗Assuming Carthage rewrites the Xcode project the way CocoaPods does
Follow-up questions
- →When would you still pick CocoaPods over SPM for a new dependency today?
- →How does SPM pull in a precompiled binary framework as a dependency?
MiddleTheoryCommonHow do build configurations, .xcconfig files, and schemes differ, and how do you ship a Staging build?
How do build configurations, .xcconfig files, and schemes differ, and how do you ship a Staging build?
A build configuration is a named set of build settings; an .xcconfig supplies them as text outside the project file. A scheme picks which config an action runs. Staging adds its own config and .xcconfig: bundle id, endpoint, signing.
Common mistakes
- ✗Conflating schemes (which action runs) with build configurations (the settings set)
- ✗Thinking only Debug and Release configurations are possible
- ✗Hard-coding environment switches in Info.plist instead of per-configuration settings
Follow-up questions
- →Why keep secrets out of
.xcconfigfiles that are committed to Git? - →How does a scheme's Archive action decide which build configuration to use?
MiddleDebuggingCommonSigning works locally but fails on CI with a missing-profile error
Signing works locally but fails on CI with a missing-profile error
CI is a clean machine — no login keychain — so it lacks the certificate and profile your Mac accumulated, and automatic signing needs an Apple ID session CI lacks. Fix by manual signing: install them into a keychain on the runner via fastlane match.
Open full question →Common mistakes
- ✗Assuming CI has the same keychain and profiles as a developer's Mac
- ✗Relying on automatic signing on a headless runner with no Apple ID session
- ✗Disabling code signing on CI instead of provisioning certificates securely
Follow-up questions
- →Why does automatic signing need an Apple ID session a headless runner lacks?
- →How does a tool like fastlane match keep the certificate off the runner's permanent keychain?
MiddleTheoryCommonA crash report from the beta service TestFlight is all hex addresses — what is a dSYM, and how do you symbolicate?
A crash report from the beta service TestFlight is all hex addresses — what is a dSYM, and how do you symbolicate?
A dSYM (debug symbols) maps addresses to file/function/line. Release builds strip symbols, so without its dSYM a report is raw hex. Symbolicate with the UUID-matched dSYM in Xcode's Organizer or atos. Dependencies ship their own dSYMs.
Common mistakes
- ✗Keeping or uploading the wrong build's
dSYM, so the UUIDs never match - ✗Thinking Release builds retain symbols the way Debug builds do
- ✗Forgetting that third-party precompiled frameworks need their own
dSYMs
Follow-up questions
- →How is a
dSYMmatched to the specific build that crashed? - →Why can a precompiled dependency's crash stay unsymbolicated even with your app's
dSYM?
SeniorDesignCommonDesign the CI/CD pipeline for an iOS app. Specify what runs on every pull request versus what runs only when a change lands on main; how signing secrets — certificates, provisioning profiles, API keys — stay out of the repository yet reach the build machine safely; how build configurations select the right environment per stage; and how a green main build reaches the beta service TestFlight and then the App Store. Also state what you consider an acceptable pipeline time for the PR path versus the release path, and how you keep the PR path fast while still gating every merge on tests and on signing-independent checks.
Design the CI/CD pipeline for an iOS app. Specify what runs on every pull request versus what runs only when a change lands on main; how signing secrets — certificates, provisioning profiles, API keys — stay out of the repository yet reach the build machine safely; how build configurations select the right environment per stage; and how a green main build reaches the beta service TestFlight and then the App Store. Also state what you consider an acceptable pipeline time for the PR path versus the release path, and how you keep the PR path fast while still gating every merge on tests and on signing-independent checks.
On every PR run fast signing-free checks (build, tests, lint) to gate merges. On main, also sign and archive: pull the cert/profile from a secret store (fastlane match) into a temp keychain, never committed, then ship to TestFlight, then App Store.
Common mistakes
- ✗Committing signing certificates or API keys into the repository
- ✗Running the slow signed-archive path on every PR instead of only on
main - ✗Baking secrets into
Info.plistso they ship inside the binary
Follow-up questions
- →How does a secret store inject the certificate into the runner without persisting it?
- →Which checks belong on the PR path to keep it fast yet still gate merges?
SeniorPerformanceOccasionalA clean build takes 12 minutes — what do you measure, and what actually cuts it?
A clean build takes 12 minutes — what do you measure, and what actually cuts it?
Measure first with build timing (-ftime-trace) to find slow files and phases. Annotate costly expressions to cut type inference, split into modules to isolate recompiles, and keep whole-module optimization for Release. Prune unused dependencies.
Common mistakes
- ✗Optimizing before measuring which files and phases are actually slow
- ✗Leaving whole-module optimization on for Debug, which kills incremental builds
- ✗Ignoring cross-module type inference from unannotated complex expressions
Follow-up questions
- →Why does whole-module optimization hurt incremental Debug builds specifically?
- →How do explicit module boundaries limit what a one-line edit recompiles?
SeniorDesignOccasionalYour app pulls a dozen third-party packages through the dependency manager SPM. One ships a breaking change in a minor release, your next resolve silently upgrades it, and three feature modules break at once. Leadership wants updates deliberate and isolated — one dependency's upgrade must never be able to break every module that touches it. Design how you pin, wrap, and vendor dependencies: how you constrain versions in Package.swift and lock them with Package.resolved, where you draw wrapper boundaries so third-party types don't leak across your modules, when you vendor a dependency (fork or copy it in) instead of depending on it live, and how you keep the whole graph auditable and reproducible across CI and every developer's machine.
Your app pulls a dozen third-party packages through the dependency manager SPM. One ships a breaking change in a minor release, your next resolve silently upgrades it, and three feature modules break at once. Leadership wants updates deliberate and isolated — one dependency's upgrade must never be able to break every module that touches it. Design how you pin, wrap, and vendor dependencies: how you constrain versions in Package.swift and lock them with Package.resolved, where you draw wrapper boundaries so third-party types don't leak across your modules, when you vendor a dependency (fork or copy it in) instead of depending on it live, and how you keep the whole graph auditable and reproducible across CI and every developer's machine.
Pin ranges in Package.swift, commit Package.resolved so all machines and CI resolve one graph. Wrap each library behind your protocol so its types don't leak — one upgrade touches one wrapper. Vendor critical or unmaintained ones, one at a time.
Common mistakes
- ✗Using open-ended version ranges and not committing
Package.resolved - ✗Letting third-party types leak across modules instead of wrapping them behind a boundary
- ✗Treating vendoring as free, ignoring the cost of tracking upstream fixes
Follow-up questions
- →How does committing
Package.resolveddiffer from pinning ranges inPackage.swift? - →When does an adapter boundary around a dependency cost more than it saves?
SeniorDesignOccasionalYour team wants to ship a new app version every week but must be able to stop a bad build fast, before it reaches most users. Design the release strategy end to end: how you use rings on Apple's beta service TestFlight (internal, then external beta) to catch regressions before the App Store, how you use App Store phased release to roll a version out to a growing percentage of users, and how server-controlled feature flags plus a kill switch let you turn a risky feature off without shipping a new binary or waiting on review. Explain what you watch during a rollout to decide whether to continue, pause the phased release, or trip the kill switch.
Your team wants to ship a new app version every week but must be able to stop a bad build fast, before it reaches most users. Design the release strategy end to end: how you use rings on Apple's beta service TestFlight (internal, then external beta) to catch regressions before the App Store, how you use App Store phased release to roll a version out to a growing percentage of users, and how server-controlled feature flags plus a kill switch let you turn a risky feature off without shipping a new binary or waiting on review. Explain what you watch during a rollout to decide whether to continue, pause the phased release, or trip the kill switch.
Ring rollout: internal TestFlight, external beta, App Store. Phased release ramps over ~7 days and pauses. Gate risky features with server flags and a kill switch — disable one remotely, no new build. Watch crash-free rate; pause on a regression.
Common mistakes
- ✗Treating phased release as irreversible instead of pausable
- ✗Assuming a kill switch requires shipping and reviewing a new build
- ✗Skipping TestFlight rings and exposing regressions straight to the App Store
Follow-up questions
- →Why can a server-side kill switch act faster than an expedited App Store review?
- →What drop in the crash-free rate would justify pausing a phased release?