What are build configurations, schemes and targets? How do you set up dev, staging and production?
TL;DR
- Target: a product to build (the app, a widget, a test bundle).
- Build configuration: a named set of build settings (
Debug,Release, maybeStaging). - Scheme: a recipe. It says which targets to build and which configuration to use for Run, Test, Profile, Analyze and Archive. It also sets launch arguments and environment variables.
For environments, add configurations, put settings in .xcconfig files, and create one scheme per environment.
Targets
Each target has its own settings, sources and Info.plist.
MyApp (app)
MyAppWidget (widget extension)
MyAppTests (unit tests)
MyAppUITests (UI tests)Avoid creating a separate app target per environment. Targets drift apart over time (forgotten files, different settings). Use configurations instead.
Build configurations
Xcode starts with two:
| Debug | Release | |
|---|---|---|
| Optimization | -Onone (fast builds, easy debugging) | -O (fast code) |
| Assertions | assert runs | assert is removed |
DEBUG flag | Set | Not set |
| Used for | Running from Xcode | Profiling, archiving, App Store |
Add a configuration in Project > Info > Configurations (for example duplicate Release as Staging).
xcconfig files
Keep settings in text files. They are easy to review in Git.
// Base.xcconfig
PRODUCT_NAME = MyApp
SWIFT_VERSION = 6.0
// Staging.xcconfig
#include "Base.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp.staging
API_BASE_URL = staging-api.example.com
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) STAGING
APP_DISPLAY_NAME = MyApp BetaA different bundle ID lets you install staging and production side by side.
Note: // starts a comment in xcconfig. To write a full URL, add https:// in code or use $() tricks like https:/$()/.
Reading settings in code
Expose a build setting through Info.plist:
<key>APIBaseURL</key>
<string>$(API_BASE_URL)</string>enum AppConfig {
static let apiBaseURL: URL = {
let host = Bundle.main.object(forInfoDictionaryKey: "APIBaseURL") as! String
return URL(string: "https://\(host)")!
}()
}Or use compilation conditions:
#if DEBUG
let logLevel = LogLevel.verbose
#elseif STAGING
let logLevel = LogLevel.info
#else
let logLevel = LogLevel.error
#endif#if removes code at compile time. Debug-only tools then never ship in the App Store binary.
Schemes
Edit with Product > Scheme > Edit Scheme.
| Action | Typical configuration |
|---|---|
| Run | Debug |
| Test | Debug |
| Profile | Release |
| Analyze | Debug |
| Archive | Release (or Staging for a beta scheme) |
Useful scheme settings:
- Arguments: launch arguments like
-com.apple.CoreData.ConcurrencyDebug 1. - Environment variables: for example
MOCK_SERVER=1. - Diagnostics: Address Sanitizer, Thread Sanitizer, Main Thread Checker.
- Test plans: which tests run, in which languages and configurations.
Mark schemes as Shared so they are saved in the project and CI can use them.
A common setup
Schemes: MyApp (Dev) -> Run: Debug, Archive: Staging
MyApp (Prod) -> Run: Release, Archive: Release
Configs: Debug, Staging, Release (each with its own .xcconfig)Secrets
Do not put real secrets in xcconfig files that are in Git. Inject them on CI, or keep them on the server. Anything in the app bundle can be read by an attacker.
Common follow-up questions
- What is the difference between
DEBUGandassert?#if DEBUGis your flag.assertis removed in optimized builds by the compiler. - What is the difference between a project and a target setting? Targets inherit project settings and can override them. xcconfig files sit under both.
- How do you build a scheme from the command line?
xcodebuild -scheme "MyApp (Prod)" -configuration Release build. - How do Swift packages handle configurations? Packages only know
debugandrelease. A custom configuration like Staging builds them as one of the two, so do not rely on custom flags inside packages. Pass values in from the app.