iosNudge

What are build configurations, schemes and targets? How do you set up dev, staging and production?

EasyTooling 3 min read

TL;DR

  • Target: a product to build (the app, a widget, a test bundle).
  • Build configuration: a named set of build settings (Debug, Release, maybe Staging).
  • 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:

DebugRelease
Optimization-Onone (fast builds, easy debugging)-O (fast code)
Assertionsassert runsassert is removed
DEBUG flagSetNot set
Used forRunning from XcodeProfiling, 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 Beta

A 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.

ActionTypical configuration
RunDebug
TestDebug
ProfileRelease
AnalyzeDebug
ArchiveRelease (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 DEBUG and assert? #if DEBUG is your flag. assert is 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 debug and release. 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.