Complete XCode Config Guide
A Complete Production-Level Guide
From Zero to Production — A beginner-friendly, step-by-step deep dive into how professional iOS teams structure their Xcode projects for Debug, Staging, and Production environments.
Topics Covered
| TARGETS | CONFIGURATIONS | SCHEMES |
|---|---|---|
| Build products, dependencies, and source file organization | Build settings per environment: Debug, Staging, Release | Tying it all together: run, test, archive, profile |
Chapter 1: Introduction — The Big Picture
1.1 What Problem Are We Solving?
Imagine you are building an iOS app. You have one version of the app that connects to your real live servers — the ones actual users will use. But you also need a version for testing, where it is safe to experiment without affecting real users. And you need yet another version for your developers to work on daily, with extra debugging tools turned on.
Without a proper setup, developers often do things like this:
// ❌ The WRONG way — manually changing URLs every time
// Developer has to remember to change this before releasing!
let apiBaseURL = "https://dev.myapp.com/api" // Development
// let apiBaseURL = "https://staging.myapp.com/api" // Staging
// let apiBaseURL = "https://api.myapp.com/api" // Production
This is dangerous. Developers forget to change it. You ship a production app pointing at the dev server. Users see test data. Disasters happen.
Xcode Configurations, Schemes, and Targets solve this problem elegantly. Once set up correctly, your app automatically uses the right server, the right API keys, the right feature flags — all based on which version you are building — with zero manual intervention.
1.2 The Three Pillars — A Simple Analogy
🏗️ Think of It Like Building a House
TARGETS = The blueprint for WHAT you are building (a house, a garage, a guest cottage). Each target produces a separate product.
CONFIGURATIONS = The building SPECIFICATIONS for each blueprint (luxury spec, standard spec, budget spec). These control HOW each product is built.
SCHEMES = The CONSTRUCTION MANAGER who decides: which blueprint to use, which spec to apply, and what to do after it is built (test it, inspect it, hand it over).
In technical terms:
- Targets: Define what gets built — one target = one app bundle (
.app), framework (.framework), or test suite (.xctest) - Configurations: Define how it gets built — the build settings (compiler flags, optimization level, code signing identity, preprocessor macros, etc.)
- Schemes: Define the workflow — which actions (Run, Test, Profile, Archive) use which target and which configuration
1.3 A Typical Production App Setup
Here is what a well-structured production iOS project looks like when you are done with this guide:
| Environment | Configuration | Scheme |
|---|---|---|
| Development (daily work) | Debug | MyApp-Dev |
| QA / Testing | Staging | MyApp-Staging |
| App Store Release | Release | MyApp-Production |
Each scheme uses a different configuration, which injects different build settings into the build. So your app automatically picks up the right API URL, right bundle ID, right app name, and right analytics keys — without a single line of code change.
Chapter 2: Targets — What You Are Building
2.1 What Is a Target?
A Target is Xcode's instruction set for building one specific product. Every target specifies:
- Which source code files to compile
- Which resources (images, sounds, storyboards) to include
- Which frameworks and libraries to link against
- What build settings (compiler flags, signing, etc.) to use
- What type of product to produce (app, framework, test bundle, extension, etc.)
Every Xcode project has at least one target. A brand-new iOS project gives you:
MyApp— The main application target (producesMyApp.app)MyAppTests— A unit test target (producesMyAppTests.xctest)MyAppUITests— A UI test target (producesMyAppUITests.xctest)
2.2 Anatomy of a Target
When you click on a target in Xcode, you see several tabs:
| Tab | What It Controls |
|---|---|
| General | App version, deployment target, device family, linked frameworks, embedded content |
| Signing & Capabilities | Your development team, bundle identifier, provisioning profiles, and special capabilities like Push Notifications, iCloud, App Groups, etc. |
| Resource Tags | On-demand resources tagging for large app assets |
| Info | Contents of the Info.plist file — app name, permissions descriptions, URL schemes, etc. |
| Build Settings | Every single compiler, linker, signing, and packaging setting. This is where most of the configuration magic happens. |
| Build Phases | The steps Xcode runs to build the target: Compile Sources, Link Binary, Copy Bundle Resources, Run Script phases, etc. |
| Build Rules | Custom rules for processing specific file types (rarely needed) |
2.3 Viewing All Targets in Your Project
Step-by-Step:
- Open your project in Xcode
- In the left panel (Project Navigator), click the blue project icon at the very top
- The main editor area shows the Project and Targets list on the left side
- You will see your project at the top (gear icon) and targets listed below it
💡 KEY DIFFERENCE: Project vs Target Settings
The PROJECT level has settings that apply to ALL targets as a baseline default. The TARGET level has settings that OVERRIDE the project defaults for that specific target. Settings at the target level ALWAYS take priority over project-level settings. Think of project settings as the 'parent' and target settings as the 'child' that can override.
2.4 Why Would You Have Multiple App Targets?
Most apps only need ONE main app target. However, professional apps sometimes have multiple:
- App Extensions: A widget target, a share extension target, a notification content extension — each is a separate target because each produces a separate bundle
- White-label apps: If you build the same app for multiple clients with different branding, you might have one target per client
- tvOS / watchOS companion apps: Each platform requires its own target
- A/B testing variants: Sometimes teams create separate targets for major feature testing (though configurations are usually better for this)
⚠️ Important Recommendation
For most apps, DO NOT create multiple targets just to handle Debug/Staging/Production environments. That is what CONFIGURATIONS are for (Chapter 3). Using multiple targets for environments leads to duplication, maintenance nightmares, and inconsistency. The correct approach: one app target + multiple configurations + multiple schemes.
2.5 Creating an App Extension Target (Example)
Step-by-Step: Adding a Widget Extension Target
- In Xcode, go to File > New > Target...
- In the template picker, select Widget Extension under the iOS section
- Fill in the options:
- Product Name:
MyAppWidget - Team: your development team
- Bundle Identifier:
com.yourcompany.myapp.widget - Include Configuration Intent: check if you want user-configurable widgets
- Product Name:
- Click Finish
- Xcode asks if you want to activate the scheme for this target. Click Activate
Xcode has now created a new target with its own:
- Build settings (can be different from the main app)
- Info.plist (
MyAppWidget-Info.plist) - Source files (
MyAppWidget.swift,MyAppWidgetBundle.swift) - Signing settings (must use the same Team ID as the main app, but a different bundle ID)
2.6 Target Dependencies
When one target depends on another being built first, you declare a Target Dependency. This ensures Xcode builds things in the right order.
Project: MyApp
├── Target: MyNetworkingKit (a framework target)
├── Target: MyApp (main app target)
│ └── Dependencies: MyNetworkingKit ← built first!
├── Target: MyAppWidget
│ └── Dependencies: MyNetworkingKit ← also built first!
├── Target: MyAppTests
│ └── Dependencies: MyApp ← app built before tests
└── Target: MyAppUITests
└── Dependencies: MyApp ← app built before UI tests
How to Add a Target Dependency:
- Select the dependent target (the one that needs something else first)
- Go to the Build Phases tab
- Click the + button at the top of the Build Phases area
- Select New Dependencies (NOT "Link Binary with Libraries" — that is different)
- Pick the target that needs to be built first
2.7 Build Phases — The Build Pipeline
Build Phases define the sequence of steps Xcode executes to build your target:
| Build Phase | What It Does |
|---|---|
| Dependencies | Ensures required targets are built before this one starts |
| Compile Sources | Runs the Swift/Objective-C compiler on your .swift and .m files |
| Link Binary With Libraries | Tells the linker which frameworks and libraries to link into your binary |
| Copy Bundle Resources | Copies your assets into the .app bundle |
| Embed Frameworks | Copies and code-signs any dynamic frameworks or app extensions into your bundle |
| Run Script | Executes a shell script at build time (SwiftLint, code generation, etc.) |
Adding a Run Script Phase (e.g., SwiftLint)
- Select your target, go to Build Phases
- Click + at the top left, choose New Run Script Phase
- Drag the new phase to just after the Compile Sources phase
- Paste your script:
#!/bin/bash
# Run SwiftLint on all source files
if which swiftlint > /dev/null; then
swiftlint
else
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi
- Check "Based on dependency analysis" to only run when relevant files change (faster builds)
- Add your Swift source files under "Input Files" for precise change tracking:
# Input Files (for change-based rebuilding)
$(SRCROOT)/MyApp/**/*.swift
Chapter 3: Configurations — Build Environments
3.1 What Is a Build Configuration?
A Build Configuration is a named collection of build settings. Think of it as a "named profile" that controls exactly how your code is compiled, signed, and packaged.
By default, every new Xcode project comes with exactly two configurations:
| Configuration | Default Purpose |
|---|---|
| Debug | Used while developing. Compiler optimizations OFF. Debug symbols included. App runs slower but is easier to debug. |
| Release | Used for App Store / TestFlight distribution. Compiler optimizations ON. Debug symbols stripped. |
For production-level apps, we add a third configuration:
| Configuration | Production Purpose |
|---|---|
| Debug | Daily development — connects to local or dev server, all debug tools enabled, verbose logging |
| Staging | QA / testing — connects to staging server that mirrors production, some debug tools, internal TestFlight builds |
| Release | Production — connects to production server, no debug tools, App Store builds |
3.2 How Build Settings Are Resolved (The Hierarchy)
Build settings in Xcode are resolved through a hierarchy, from lowest to highest priority:
🎯 Build Setting Priority (Lowest → Highest)
- Xcode's built-in defaults (always the starting point)
- xcconfig file at the project level (if assigned to the project)
- Project-level build settings (set in the Project > Build Settings tab)
- xcconfig file at the target level (if assigned to the target)
- Target-level build settings (set in the Target > Build Settings tab)
HIGHER NUMBER = WINS. Target settings always beat project settings. xcconfig files are applied BEFORE the inline GUI settings at each level.
3.3 Viewing and Adding Configurations
Step-by-Step: Adding a Staging Configuration
- Click the blue project icon in the Project Navigator
- Click the project name (not a target) in the left side of the editor. You will see an "Info" tab appear at the top
- Under the Configurations section, you will see Debug and Release already listed
- Click the + button at the bottom of the Configurations section
- Choose "Duplicate "Release" Configuration" — we base Staging on Release because it should be close to production behavior
- Name it Staging
You now have three configurations: Debug, Staging, and Release. Each configuration exists for EVERY target in your project.
💡 Why Duplicate Release, Not Debug?
Staging should behave as close to production as possible so you catch real bugs. If you duplicate Debug, your Staging builds have compiler optimizations OFF and debug symbols ON — this hides real-world performance issues. By duplicating Release, your Staging app behaves like a production app, just pointing at a different server.
3.4 Build Settings — The Heart of Configurations
How to View Build Settings:
- Select your app target
- Click the Build Settings tab
- At the top, click All (not Basic) to see every setting
- Click Combined to see both project and target settings together
- Notice the column headers — they show values per configuration:
Debug|Staging|Release
Key Build Settings to Configure Per Environment:
| Build Setting (Key) | What It Controls |
|---|---|
SWIFT_OPTIMIZATION_LEVEL |
Swift compiler optimization. Set to -Onone for Debug, -O for Staging/Release |
GCC_OPTIMIZATION_LEVEL |
Objective-C compiler optimization. 0 for Debug, s for Release |
SWIFT_ACTIVE_COMPILATION_CONDITIONS |
Preprocessor flags for Swift. Add DEBUG for Debug config, STAGING for Staging |
GCC_PREPROCESSOR_DEFINITIONS |
Preprocessor macros for ObjC/C. Add DEBUG=1 in debug config |
DEBUG_INFORMATION_FORMAT |
dwarf for Debug (faster builds), dwarf-with-dsym for Release (enables crash symbolication) |
ENABLE_TESTABILITY |
YES for Debug/Staging, NO for Release (improves binary size and performance) |
VALIDATE_PRODUCT |
NO for Debug (faster builds), YES for Release |
CODE_SIGN_IDENTITY |
iPhone Developer for Debug, iPhone Distribution for Release |
PROVISIONING_PROFILE_SPECIFIER |
Which provisioning profile to use. Different for dev vs distribution |
PRODUCT_BUNDLE_IDENTIFIER |
The bundle ID. Can be different per config for side-by-side installs |
3.5 User-Defined Build Settings — Your Custom Variables
Step-by-Step: Creating a Custom Build Setting for the API URL
- Select your app target, go to Build Settings
- Click + at the top of the Build Settings area
- Choose Add User-Defined Setting
- Name it
API_BASE_URL(all caps, underscores — this is convention) - Fill in values for each configuration:
API_BASE_URL
Debug: https://dev.api.myapp.com
Staging: https://staging.api.myapp.com
Release: https://api.myapp.com
- Do the same for other environment-specific values:
ANALYTICS_KEY
Debug: dev_analytics_key_123
Staging: staging_analytics_key_456
Release: prod_analytics_key_789
ENABLE_CRASH_REPORTING
Debug: NO
Staging: YES
Release: YES
APP_DISPLAY_NAME
Debug: MyApp (Dev)
Staging: MyApp (Staging)
Release: MyApp
3.6 Connecting Build Settings to Info.plist
The bridge between build settings and your running app is the Info.plist file. You reference build settings using the $(SETTING_NAME) syntax, and Xcode substitutes the actual value at build time.
Step-by-Step: Passing API URL to Info.plist
- Open your
Info.plistfile - Right-click anywhere and choose Add Row
- Name the key
APIBaseURL - Set its type to String
- Set its value to
$(API_BASE_URL)
At build time, Xcode replaces $(API_BASE_URL) with the actual value from the current configuration's build settings.
Reading the Value in Swift:
// ✅ The RIGHT way — read from Info.plist at runtime
// Values are automatically set correctly for each build configuration
enum AppConfig {
static var apiBaseURL: URL {
guard
let urlString = Bundle.main.infoDictionary?["APIBaseURL"] as? String,
let url = URL(string: urlString)
else {
fatalError("APIBaseURL not found in Info.plist — check build settings")
}
return url
}
static var analyticsKey: String {
guard let key = Bundle.main.infoDictionary?["AnalyticsKey"] as? String else {
fatalError("AnalyticsKey not found in Info.plist")
}
return key
}
}
// Usage:
let url = AppConfig.apiBaseURL
// Debug build → https://dev.api.myapp.com
// Staging build → https://staging.api.myapp.com
// Release build → https://api.myapp.com
3.7 Swift Compilation Conditions (#if DEBUG)
The build setting SWIFT_ACTIVE_COMPILATION_CONDITIONS lets you define flags that exist only in certain configurations. These allow you to include or exclude code at compile time.
Setting Up Compilation Conditions:
- Go to Target > Build Settings > Swift Compiler - Custom Flags > Active Compilation Conditions
- Set the values:
Debug: DEBUG
Staging: STAGING
Release: (empty)
Now you can write Swift code that only compiles in specific environments:
// Code that only exists in debug builds
#if DEBUG
print("[DEBUG] Making network request to: \(url)")
NetworkLogger.shared.isEnabled = true
#endif
// Code for both debug AND staging (useful for internal tools)
#if DEBUG || STAGING
ShowInternalMenu.isEnabled = true
#endif
// Code only in staging
#if STAGING
let serverEnvironment = "Staging"
ShowStagingBanner.isEnabled = true
#endif
// Production-only code
#if !DEBUG && !STAGING
Analytics.trackAppLaunch()
CrashReporter.start()
#endif
⚠️ Important:
#ifvsif
#if DEBUGis a compile-time check — the code inside literally does not exist in the binary if the flag is not set.if someFlagis a runtime check — the code exists in the binary but is not executed. Use#iffor performance-sensitive code, security-sensitive code, and code that should never ship to users.
3.8 Setting Different Bundle IDs Per Configuration
Giving each configuration a different Bundle ID lets you install Debug, Staging, and Release builds on the SAME device at the same time — they appear as separate apps!
Step-by-Step:
- Go to your target > Build Settings
- Search for Product Bundle Identifier
- Set different values per configuration:
PRODUCT_BUNDLE_IDENTIFIER
Debug: com.yourcompany.myapp.debug
Staging: com.yourcompany.myapp.staging
Release: com.yourcompany.myapp
- Set the app display name per configuration in
Info.plist:
Key: CFBundleDisplayName
Value: $(APP_DISPLAY_NAME)
// And in Build Settings, create a User-Defined setting:
APP_DISPLAY_NAME
Debug: MyApp DEV
Staging: MyApp STAGING
Release: MyApp
Now your phone shows three separate app icons — "MyApp DEV", "MyApp STAGING", and "MyApp" — all installed side-by-side.
Chapter 4: xcconfig Files — Managing Settings at Scale
4.1 What Is an xcconfig File?
An xcconfig (Xcode Configuration) file is a plain text file that stores build settings. It is an alternative to (and works alongside) setting build settings directly in the Xcode GUI.
Why xcconfig files are preferred in professional projects:
- Version Control: Plain text files are perfectly trackable in Git. You can see exactly what changed in a PR
- Readability: All your settings are in one place, clearly visible, not buried in Xcode GUI tabs
- CocoaPods/SPM compatibility: CocoaPods generates xcconfig files and expects you to include them
- No merge conflicts: The binary
.pbxprojfile gets enormous and causes confusing merge conflicts. xcconfig files are simple text - Reusability: You can
#includeone xcconfig from another, creating a clean inheritance hierarchy
4.2 xcconfig File Syntax
// This is a comment in an xcconfig file
// Simple setting
SWIFT_VERSION = 5.9
// Setting with spaces
PRODUCT_NAME = My App
// Setting that references another setting using $()
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.$(PRODUCT_NAME:lower:identifier)
// Include another xcconfig file (inheritance!)
#include "Shared.xcconfig"
// Include a Pods-generated xcconfig (CocoaPods integration)
#include "Pods/Target Support Files/Pods-MyApp/Pods-MyApp.debug.xcconfig"
// Conditional: only apply on simulator
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
4.3 Recommended xcconfig File Structure
MyApp/
├── MyApp.xcodeproj/
├── MyApp/ ← Source code
│ ├── AppDelegate.swift
│ └── ...
├── Configurations/ ← NEW FOLDER for xcconfig files
│ ├── Base/
│ │ ├── Shared.xcconfig ← Settings for ALL configs
│ │ ├── App.xcconfig ← App-specific base settings
│ │ └── Tests.xcconfig ← Test target base settings
│ ├── Debug/
│ │ ├── Debug.xcconfig ← Shared debug settings
│ │ └── App-Debug.xcconfig ← App target debug settings
│ ├── Staging/
│ │ ├── Staging.xcconfig ← Shared staging settings
│ │ └── App-Staging.xcconfig ← App target staging settings
│ └── Release/
│ ├── Release.xcconfig ← Shared release settings
│ └── App-Release.xcconfig ← App target release settings
└── Podfile ← CocoaPods (if used)
4.4 Creating the xcconfig Files
Step-by-Step: Creating an xcconfig File in Xcode
- Right-click on your project in the Project Navigator
- Choose New File...
- Scroll down to find Configuration Settings File (or search for "xcconfig")
- Click Next, name it (e.g.,
Shared.xcconfig), and save it in yourConfigurations/Base/folder - Make sure the file is NOT added to any target — uncheck all targets in the dialog
Contents of Each File:
Configurations/Base/Shared.xcconfig — Settings that apply to ALL environments:
// Configurations/Base/Shared.xcconfig
// Settings shared across ALL configurations
SWIFT_VERSION = 5.9
IPHONEOS_DEPLOYMENT_TARGET = 16.0
TARGETED_DEVICE_FAMILY = 1,2
ALWAYS_SEARCH_USER_PATHS = NO
CLANG_ENABLE_MODULES = YES
SWIFT_OBJC_INTEROP_MODE = objcxx
// Code quality
GCC_WARN_UNDECLARED_SELECTOR = YES
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE
GCC_WARN_UNUSED_FUNCTION = YES
SWIFT_TREAT_WARNINGS_AS_ERRORS = NO
Configurations/Base/App.xcconfig — App-specific base settings:
// Configurations/Base/App.xcconfig
#include "Shared.xcconfig"
// Product info
PRODUCT_NAME = MyApp
MARKETING_VERSION = 1.0.0
CURRENT_PROJECT_VERSION = 1
// Signing - will be overridden per environment
CODE_SIGN_STYLE = Manual
Configurations/Debug/Debug.xcconfig — Debug-specific settings:
// Configurations/Debug/Debug.xcconfig
#include "../Base/Shared.xcconfig"
// Compiler: No optimizations for debuggability
SWIFT_OPTIMIZATION_LEVEL = -Onone
GCC_OPTIMIZATION_LEVEL = 0
// Debug symbols inline (faster builds)
DEBUG_INFORMATION_FORMAT = dwarf
// Enable assertions and debug checks
ENABLE_NS_ASSERTIONS = YES
GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited)
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG
// Allow attaching debugger
ENABLE_TESTABILITY = YES
// Do NOT validate (faster builds)
VALIDATE_PRODUCT = NO
Configurations/Debug/App-Debug.xcconfig — App target in Debug:
// Configurations/Debug/App-Debug.xcconfig
#include "Debug.xcconfig"
#include "../Base/App.xcconfig"
// Different bundle ID so dev and prod can co-exist on device
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.myapp.debug
// Display name shows this is a dev build
APP_DISPLAY_NAME = MyApp DEV
// Connect to development server
API_BASE_URL = https://dev.api.myapp.com
API_VERSION = v1
// Development analytics key (won't affect real metrics)
ANALYTICS_KEY = dev_analytics_abc123
// Signing with development profile
CODE_SIGN_IDENTITY = iPhone Developer
PROVISIONING_PROFILE_SPECIFIER = MyApp Development
// Enable push notifications on development cert
PUSH_NOTIFICATION_ENV = development
Configurations/Staging/App-Staging.xcconfig — App target in Staging:
// Configurations/Staging/App-Staging.xcconfig
#include "Staging.xcconfig"
#include "../Base/App.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.myapp.staging
APP_DISPLAY_NAME = MyApp STAGING
// Staging server
API_BASE_URL = https://staging.api.myapp.com
API_VERSION = v1
// Staging analytics (separate property in analytics dashboard)
ANALYTICS_KEY = staging_analytics_def456
// Distribution signing for TestFlight
CODE_SIGN_IDENTITY = iPhone Distribution
PROVISIONING_PROFILE_SPECIFIER = MyApp Staging Ad Hoc
PUSH_NOTIFICATION_ENV = production
Configurations/Release/App-Release.xcconfig — App target in Release:
// Configurations/Release/App-Release.xcconfig
#include "Release.xcconfig"
#include "../Base/App.xcconfig"
// Production bundle ID
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.myapp
APP_DISPLAY_NAME = MyApp
// Production server
API_BASE_URL = https://api.myapp.com
API_VERSION = v1
// Production analytics
ANALYTICS_KEY = prod_analytics_ghi789
// App Store signing
CODE_SIGN_IDENTITY = iPhone Distribution
PROVISIONING_PROFILE_SPECIFIER = MyApp App Store
PUSH_NOTIFICATION_ENV = production
4.5 Assigning xcconfig Files to Configurations
Step-by-Step:
- Click the blue project icon in the Project Navigator
- Click the project name (not a target)
- Click the Info tab
- Under Configurations, you will see the three configs with sub-rows for the project and each target
- For the project row under Debug: click the dropdown and select
Debug.xcconfig - For MyApp target under Debug: select
App-Debug.xcconfig - For MyApp target under Staging: select
App-Staging.xcconfig - For MyApp target under Release: select
App-Release.xcconfig
| Configuration | Project Level | MyApp Target |
|---|---|---|
| Debug | Configurations/Debug/Debug.xcconfig |
Configurations/Debug/App-Debug.xcconfig |
| Staging | Configurations/Staging/Staging.xcconfig |
Configurations/Staging/App-Staging.xcconfig |
| Release | Configurations/Release/Release.xcconfig |
Configurations/Release/App-Release.xcconfig |
⚠️ Critical: The
$(inherited)VariableWhen you have xcconfig files that include other xcconfig files AND you also set values in the Xcode GUI Build Settings, you need to use
$(inherited)to merge them.The
$(inherited)expands to whatever was set at the lower priority level. Without$(inherited), you would REPLACE the inherited value entirely instead of adding to it. CocoaPods xcconfig files heavily rely on$(inherited)— always include it forOTHER_LDFLAGS,HEADER_SEARCH_PATHS, andFRAMEWORK_SEARCH_PATHS.
4.6 The $(inherited) Pattern in Practice
// ❌ WRONG - Breaks CocoaPods and other settings
OTHER_LDFLAGS = -ObjC
// ✅ CORRECT - Adds -ObjC to whatever was already inherited
OTHER_LDFLAGS = -ObjC $(inherited)
// ❌ WRONG - Overwrites any HEADER_SEARCH_PATHS set elsewhere
HEADER_SEARCH_PATHS = $(SRCROOT)/Headers
// ✅ CORRECT
HEADER_SEARCH_PATHS = $(SRCROOT)/Headers $(inherited)
Chapter 5: Schemes — The Build Workflow
5.1 What Is a Scheme?
A Scheme is a named set of instructions that tells Xcode: "When I press Run (or Test, or Archive), build THIS target with THIS configuration and do THESE things before/after."
It answers these questions:
- Which target? (The main app? The test target? An extension?)
- Which configuration? (Debug? Staging? Release?)
- What environment variables? (Any extra vars to inject at launch time for testing?)
- What to do before building? (Pre-actions: run a script, print something to console)
- What to do after building? (Post-actions: notify Slack, upload symbols, run integration tests)
- Which tests to run? (Select specific test plans or test bundles)
5.2 Anatomy of a Scheme — The Six Actions
| Action | What It Does |
|---|---|
| Build (Cmd+B) | Builds the selected target and its dependencies |
| Run (Cmd+R) | Builds and launches the app on the simulator or device. You choose the Debug/Staging/Release configuration here |
| Test (Cmd+U) | Builds the test bundle and runs your tests. Configure code coverage collection here |
| Profile (Cmd+I) | Builds with the Release configuration and launches Instruments for performance profiling |
| Analyze (Shift+Cmd+B) | Runs the Clang static analyzer to find potential bugs (memory leaks, null pointer dereferences, etc.) |
| Archive (Product > Archive) | Creates a distributable build for App Store or Ad Hoc distribution |
5.3 Creating Production-Level Schemes
| Scheme | Target + Configuration Used |
|---|---|
| MyApp-Dev | Run: Debug, Test: Debug, Archive: Debug |
| MyApp-Staging | Run: Staging, Test: Staging, Archive: Staging |
| MyApp-Production | Run: Release, Test: Release, Archive: Release |
Step-by-Step: Creating a New Scheme
- In Xcode, go to Product > Scheme > Manage Schemes...
- You will see the existing schemes. Click + at the bottom
- In the dialog, select your main app target from the dropdown
- Name the scheme MyApp-Dev
- Click OK
- Repeat to create MyApp-Staging and MyApp-Production
5.4 Configuring Each Scheme in Detail
Step-by-Step: Configure MyApp-Dev Scheme
- In Manage Schemes, double-click MyApp-Dev to open its editor
- Click Run in the left sidebar:
- Build Configuration: Debug
- Debug executable: ✓ checked
- Launch: Automatically
- Click Test in the left sidebar:
- Build Configuration: Debug
- Add your test targets: click +, add
MyAppTestsandMyAppUITests - Code Coverage: ✓ Enable code coverage
- Click Profile:
- Build Configuration: Release (profile in release mode for real perf numbers)
- Click Archive:
- Build Configuration: Debug
- Click Close to save
Step-by-Step: Configure MyApp-Production Scheme
- Double-click MyApp-Production
- Click Run: set Build Configuration to Release
- Click Test: set Build Configuration to Release
- Click Archive: set Build Configuration to Release
5.5 Environment Variables and Launch Arguments
Environment Variables
These are key-value pairs injected into the process environment at launch time. They are ONLY used when running from Xcode — NOT in production builds.
// In Scheme > Run > Environment Variables:
// Name Value
// ─────────────────────────────────────────────
// OS_ACTIVITY_MODE disable (suppress system logs in console)
// SQLITE_DEBUG 1 (enable SQLite verbose logging)
// IDEPreferLogStreaming YES (better Xcode console output)
// Reading in Swift:
if let mode = ProcessInfo.processInfo.environment["SQLITE_DEBUG"] {
SQLiteLogger.isEnabled = (mode == "1")
}
Launch Arguments
Arguments passed to the app's argv. Several Apple frameworks recognize specific launch arguments:
// Scheme > Run > Arguments Passed On Launch:
-com.apple.CoreData.ConcurrencyDebug 1 // Detect Core Data threading bugs
-com.apple.CoreData.SQLDebug 1 // Log all Core Data SQL queries
-UIViewLayoutFeedbackLoopDebuggingThreshold 100 // Detect Auto Layout loops
-NSDoubleLocalizedStrings YES // Double all localized strings (test layouts)
-AppleLanguages (ar) // Test Arabic right-to-left layout
-AppleLocale ar_SA // Test Saudi Arabic locale
-FIRDebugEnabled // Enable Firebase debug mode
5.6 Pre-Actions and Post-Actions
Step-by-Step: Add a Pre-Action to Print Git Info
- Open scheme editor, click Build in the sidebar
- Click the triangle/disclosure next to Build to reveal Pre-actions and Post-actions
- Click + under Pre-actions
- Choose New Run Script Action
- Under "Provide build settings from", choose your app target (important! otherwise
$SRCROOTwon't be set) - Paste the script:
#!/bin/bash
# Pre-build: Print environment info
echo "========================================"
echo "Building: $PRODUCT_NAME"
echo "Configuration: $CONFIGURATION"
echo "Bundle ID: $PRODUCT_BUNDLE_IDENTIFIER"
echo "API URL: $API_BASE_URL"
echo "Git branch: $(git -C "$SRCROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')"
echo "Git commit: $(git -C "$SRCROOT" rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
echo "========================================"
Post-Action: Auto-increment build number on Archive
#!/bin/bash
# Post-archive: Increment build number in Info.plist
PLIST="$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH"
BUILD_NUMBER=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$PLIST")
NEW_BUILD=$((BUILD_NUMBER + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $NEW_BUILD" "$PLIST"
echo "Build number bumped from $BUILD_NUMBER to $NEW_BUILD"
5.7 Shared vs Personal Schemes
| Scheme Type | Behavior |
|---|---|
| Shared (Recommended for teams) | Stored in MyApp.xcodeproj/xcshareddata/xcschemes/ and committed to Git. Every developer who clones the repo gets the same schemes automatically. |
| Personal (Default) | Stored in MyApp.xcodeproj/xcuserdata/username.xcuserdatad/xcschemes/ and NOT committed to Git. Only visible to the current user. |
Making a Scheme Shared:
- Go to Product > Scheme > Manage Schemes...
- Check the Shared checkbox next to each scheme
- Commit the xcshareddata folder:
git add MyApp.xcodeproj/xcshareddata/
💡
.gitignoreRecommendation# Ignore personal scheme files: *.xcodeproj/xcuserdata/ *.xcworkspace/xcuserdata/ # DO NOT ignore shared schemes: # *.xcodeproj/xcshareddata/ ← keep this out of gitignore
Chapter 6: Complete Production Setup — Step by Step
📋 Project Specification
App Name: MyApp | Company: YourCompany | Bundle ID base:
com.yourcompany.myappEnvironments: Development, Staging, Production Features: In-app purchases (production only), internal debug menu (dev only)
6.1 Phase 1 — Project Setup
Step 1: Create the Xcode Project
- Open Xcode, click Create a new Xcode project
- Choose App under iOS
- Fill in: Product Name:
MyApp, Organization Identifier:com.yourcompany, Language: Swift - Choose a location and click Create
Step 2: Initialize Git
cd /path/to/your/project
git init
git add .
git commit -m "Initial Xcode project"
6.2 Phase 2 — Add the Staging Configuration
Step 3: Add the Staging Configuration
- Click the project icon in the navigator
- Click the project name under PROJECT (not TARGETS)
- Click the Info tab
- Under Configurations, click + > Duplicate "Release" Configuration
- Name it: Staging
Verification: You should now see exactly three configurations: Debug, Staging, Release.
6.3 Phase 3 — Create xcconfig Files
Step 4: Create the Configurations folder structure
# In Terminal, from your project root:
mkdir -p Configurations/Base
mkdir -p Configurations/Debug
mkdir -p Configurations/Staging
mkdir -p Configurations/Release
Step 5: Add the folder to Xcode
- Right-click your project folder in the Navigator
- Choose Add Files to "MyApp"...
- Navigate to the
Configurationsfolder - Select it, make sure "Create groups" is selected (not references)
- Make sure no targets are checked in the Add to targets section
- Click Add
Step 6: Create the xcconfig files in Xcode
For each xcconfig file:
- Right-click the appropriate folder in Navigator > New File...
- Choose Configuration Settings File
- Name it and save (make sure no targets are checked)
Step 7: Populate the xcconfig files
Configurations/Base/Shared.xcconfig:
SWIFT_VERSION = 5.9
IPHONEOS_DEPLOYMENT_TARGET = 16.0
TARGETED_DEVICE_FAMILY = 1,2
ALWAYS_SEARCH_USER_PATHS = NO
CLANG_ENABLE_MODULES = YES
CLANG_ENABLE_OBJC_ARC = YES
ENABLE_STRICT_OBJC_MSGSEND = YES
GCC_C_LANGUAGE_STANDARD = gnu11
GCC_NO_COMMON_BLOCKS = YES
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR
GCC_WARN_UNDECLARED_SELECTOR = YES
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE
GCC_WARN_UNUSED_FUNCTION = YES
GCC_WARN_UNUSED_VARIABLE = YES
Configurations/Debug/Debug.xcconfig:
#include "../Base/Shared.xcconfig"
SWIFT_OPTIMIZATION_LEVEL = -Onone
GCC_OPTIMIZATION_LEVEL = 0
ONLY_ACTIVE_ARCH = YES
DEBUG_INFORMATION_FORMAT = dwarf
ENABLE_NS_ASSERTIONS = YES
ENABLE_TESTABILITY = YES
VALIDATE_PRODUCT = NO
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG
GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited)
Configurations/Debug/App-Debug.xcconfig:
#include "Debug.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.myapp.debug
PRODUCT_NAME = MyApp
APP_DISPLAY_NAME = MyApp (Dev)
API_BASE_URL = https://dev.api.myapp.com
API_VERSION = v1
ANALYTICS_KEY = dev_amp_key_abc
FEATURE_IAP_ENABLED = NO
FEATURE_DEBUG_MENU = YES
ENABLE_CRASH_REPORTING = NO
CODE_SIGN_STYLE = Automatic
CODE_SIGN_IDENTITY = iPhone Developer
Configurations/Staging/Staging.xcconfig:
#include "../Base/Shared.xcconfig"
// Staging is like Release but with some debug tools
SWIFT_OPTIMIZATION_LEVEL = -O
GCC_OPTIMIZATION_LEVEL = s
ONLY_ACTIVE_ARCH = NO
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
ENABLE_NS_ASSERTIONS = YES
ENABLE_TESTABILITY = NO
VALIDATE_PRODUCT = YES
MTL_ENABLE_DEBUG_INFO = NO
SWIFT_ACTIVE_COMPILATION_CONDITIONS = STAGING
GCC_PREPROCESSOR_DEFINITIONS = $(inherited)
Configurations/Staging/App-Staging.xcconfig:
#include "Staging.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.myapp.staging
PRODUCT_NAME = MyApp
APP_DISPLAY_NAME = MyApp (Staging)
API_BASE_URL = https://staging.api.myapp.com
API_VERSION = v1
ANALYTICS_KEY = staging_amp_key_def
FEATURE_IAP_ENABLED = YES
FEATURE_DEBUG_MENU = YES
ENABLE_CRASH_REPORTING = YES
CODE_SIGN_STYLE = Manual
CODE_SIGN_IDENTITY = iPhone Distribution
PROVISIONING_PROFILE_SPECIFIER = MyApp Staging
Configurations/Release/Release.xcconfig:
#include "../Base/Shared.xcconfig"
SWIFT_OPTIMIZATION_LEVEL = -O
GCC_OPTIMIZATION_LEVEL = s
ONLY_ACTIVE_ARCH = NO
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
ENABLE_NS_ASSERTIONS = NO
ENABLE_TESTABILITY = NO
VALIDATE_PRODUCT = YES
MTL_ENABLE_DEBUG_INFO = NO
SWIFT_ACTIVE_COMPILATION_CONDITIONS =
GCC_PREPROCESSOR_DEFINITIONS = $(inherited)
SWIFT_COMPILATION_MODE = wholemodule
Configurations/Release/App-Release.xcconfig:
#include "Release.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.myapp
PRODUCT_NAME = MyApp
APP_DISPLAY_NAME = MyApp
API_BASE_URL = https://api.myapp.com
API_VERSION = v1
ANALYTICS_KEY = prod_amp_key_ghi
FEATURE_IAP_ENABLED = YES
FEATURE_DEBUG_MENU = NO
ENABLE_CRASH_REPORTING = YES
CODE_SIGN_STYLE = Manual
CODE_SIGN_IDENTITY = iPhone Distribution
PROVISIONING_PROFILE_SPECIFIER = MyApp App Store
6.4 Phase 4 — Assign xcconfig Files
Step 8: Assign xcconfig files to configurations
- Click the project icon > project name > Info tab
- Expand each configuration and assign:
| Configuration | Project Level | MyApp Target |
|---|---|---|
| Debug | Debug/Debug.xcconfig |
Debug/App-Debug.xcconfig |
| Staging | Staging/Staging.xcconfig |
Staging/App-Staging.xcconfig |
| Release | Release/Release.xcconfig |
Release/App-Release.xcconfig |
6.5 Phase 5 — Configure Info.plist
Step 9: Update Info.plist to use build setting variables
| Info.plist Key | Value to Set |
|---|---|
CFBundleDisplayName |
$(APP_DISPLAY_NAME) |
CFBundleIdentifier |
$(PRODUCT_BUNDLE_IDENTIFIER) |
CFBundleVersion |
$(CURRENT_PROJECT_VERSION) |
CFBundleShortVersionString |
$(MARKETING_VERSION) |
APIBaseURL |
$(API_BASE_URL) |
APIVersion |
$(API_VERSION) |
AnalyticsKey |
$(ANALYTICS_KEY) |
FeatureIAPEnabled |
$(FEATURE_IAP_ENABLED) |
EnableCrashReporting |
$(ENABLE_CRASH_REPORTING) |
6.6 Phase 6 — Write the Swift Configuration Layer
Step 10: Create AppConfiguration.swift
// AppConfiguration.swift
// This file is the SINGLE SOURCE OF TRUTH for all environment config.
import Foundation
// MARK: - Environment Enum
enum AppEnvironment {
case debug
case staging
case production
var displayName: String {
switch self {
case .debug: return "Development"
case .staging: return "Staging"
case .production: return "Production"
}
}
var isProduction: Bool { self == .production }
var isDevelopment: Bool { self == .debug }
}
// MARK: - AppConfiguration
struct AppConfiguration {
// ── Singleton ─────────────────────────────────────
static let shared = AppConfiguration()
private init() {}
// ── Environment Detection ─────────────────────────
var environment: AppEnvironment {
#if DEBUG
return .debug
#elseif STAGING
return .staging
#else
return .production
#endif
}
// ── Info.plist Reader ─────────────────────────────
private func infoPlistValue<T>(for key: String) -> T {
guard let value = Bundle.main.infoDictionary?[key] as? T else {
fatalError("\(key) not found in Info.plist. Check xcconfig files.")
}
return value
}
// ── Network ───────────────────────────────────────
var apiBaseURL: URL {
let urlString: String = infoPlistValue(for: "APIBaseURL")
guard let url = URL(string: urlString) else {
fatalError("Invalid APIBaseURL: \(urlString)")
}
return url
}
var apiVersion: String { infoPlistValue(for: "APIVersion") }
var fullAPIURL: URL { apiBaseURL.appendingPathComponent(apiVersion) }
// ── Analytics ─────────────────────────────────────
var analyticsKey: String { infoPlistValue(for: "AnalyticsKey") }
// ── Feature Flags ─────────────────────────────────
var isIAPEnabled: Bool {
let value: String = infoPlistValue(for: "FeatureIAPEnabled")
return value == "YES"
}
var isCrashReportingEnabled: Bool {
let value: String = infoPlistValue(for: "EnableCrashReporting")
return value == "YES"
}
// ── Debug Menu (code literally absent from production binary) ──
var isDebugMenuEnabled: Bool {
#if DEBUG || STAGING
return true
#else
return false
#endif
}
var isVerboseLoggingEnabled: Bool {
#if DEBUG
return true
#else
return false
#endif
}
}
Usage in your app:
// In AppDelegate or App struct:
let config = AppConfiguration.shared
// Network layer:
let baseURL = config.fullAPIURL // https://dev.api.myapp.com/v1
// Analytics setup:
Amplitude.instance().initializeApiKey(config.analyticsKey)
// Crash reporting:
if config.isCrashReportingEnabled {
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true)
}
// Environment banner (show in debug/staging to avoid confusion):
#if DEBUG || STAGING
EnvironmentBannerView.show(config.environment.displayName)
#endif
6.7 Phase 7 — Create and Configure Schemes
Step 11: Create the three schemes
- Go to Product > Scheme > Manage Schemes...
- Delete the auto-generated MyApp scheme (or rename it)
- Click + three times to create:
- MyApp-Dev — target: MyApp
- MyApp-Staging — target: MyApp
- MyApp-Production — target: MyApp
- Check Shared for all three schemes
Step 12: Configure each scheme's actions
| Action | MyApp-Dev | MyApp-Staging | MyApp-Production |
|---|---|---|---|
| Run | Debug | Staging | Release |
| Test | Debug | Staging | Release |
| Profile | Release | Release | Release |
| Analyze | Debug | Staging | Release |
| Archive | Debug | Staging | Release |
6.8 Phase 8 — Verification
Step 13: Verify the setup works
- In the Xcode toolbar, switch to the MyApp-Dev scheme
- Build and run (Cmd+R)
- Add a breakpoint in AppDelegate and inspect:
po AppConfiguration.shared.apiBaseURL - Confirm it shows
https://dev.api.myapp.com - Switch to MyApp-Production scheme
- Build (Cmd+B) — confirm correct bundle ID in the build log
// Quick verification — add this temporarily to AppDelegate:
override func application(...) -> Bool {
let config = AppConfiguration.shared
print("""
┌─────────────────────────────────────┐
│ App Configuration Check │
│ Environment: \(config.environment.displayName)
│ API URL: \(config.apiBaseURL)
│ Analytics: \(config.analyticsKey.prefix(8))...
│ IAP: \(config.isIAPEnabled)
│ Debug Menu: \(config.isDebugMenuEnabled)
└─────────────────────────────────────┘
""")
return true
}
Chapter 7: Advanced Topics
7.1 CocoaPods Integration with xcconfig Files
The Problem:
When you run pod install, CocoaPods generates xcconfig files like:
Pods/Target Support Files/Pods-MyApp/Pods-MyApp.debug.xcconfig
Pods/Target Support Files/Pods-MyApp/Pods-MyApp.staging.xcconfig
Pods/Target Support Files/Pods-MyApp/Pods-MyApp.release.xcconfig
CocoaPods then assigns these to your configurations. But if you ALSO have your own xcconfig files assigned, CocoaPods overwrites your assignment!
The Solution: Include CocoaPods xcconfig from YOUR xcconfig
// Configurations/Debug/App-Debug.xcconfig
// Include CocoaPods-generated settings FIRST
#include "../../Pods/Target Support Files/Pods-MyApp/Pods-MyApp.debug.xcconfig"
// Then your settings (these override CocoaPods settings if there is a conflict)
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.myapp.debug
APP_DISPLAY_NAME = MyApp (Dev)
// IMPORTANT: Always use $(inherited) for search paths and linker flags
FRAMEWORK_SEARCH_PATHS = $(inherited)
OTHER_LDFLAGS = $(inherited)
HEADER_SEARCH_PATHS = $(inherited)
Then in Xcode's configuration assignment, assign YOUR xcconfig files (not the CocoaPods ones). CocoaPods' settings flow in via the #include.
7.2 Swift Package Manager and Configurations
SPM is simpler than CocoaPods — it does not generate xcconfig files.
// App targets cannot have conditional dependencies based on configuration
// You must use #if DEBUG in Swift code instead
// ── Conditionally use debug-only tools: ──
#if DEBUG
import XCTestDynamicOverlay // Only available in debug
import InjectHot // Hot reloading (debug only)
#endif
// ── For SPM packages that are test-only: ──
// Add them to your test target, not main app target
// In Xcode: Target > General > Frameworks, Libraries > add to test target only
7.3 Secrets Management — Keeping API Keys Safe
🔒 Security Warning
NEVER commit real API keys, passwords, or secrets to Git — even in private repos. xcconfig files ARE committed to Git, so do not put secrets in them directly. Use environment variables in CI/CD, or a secrets manager like HashiCorp Vault.
Recommended Pattern: Secret xcconfig Files
# .gitignore — add these files
Configurations/Secrets/Secrets-Debug.xcconfig
Configurations/Secrets/Secrets-Staging.xcconfig
Configurations/Secrets/Secrets-Release.xcconfig
// Configurations/Secrets/Secrets-Debug.xcconfig
// ⚠️ THIS FILE IS NOT COMMITTED TO GIT
// Copy Secrets-Debug.xcconfig.template and fill in real values
STRIPE_PUBLISHABLE_KEY = pk_test_yourActualStripeKeyHere
GOOGLE_CLIENT_ID = com.yourcompany.myapp.debug
MAPS_API_KEY = AIzaSyActualKeyHere
// Configurations/Secrets/Secrets-Debug.xcconfig.template
// ✅ THIS FILE IS committed to Git as a template
STRIPE_PUBLISHABLE_KEY = pk_test_REPLACE_WITH_YOUR_KEY
GOOGLE_CLIENT_ID = com.yourcompany.myapp.debug
MAPS_API_KEY = REPLACE_WITH_YOUR_KEY
Generate secrets in CI/CD before building:
# CI script: generate-secrets.sh
#!/bin/bash
cat > Configurations/Secrets/Secrets-Release.xcconfig << EOF
STRIPE_PUBLISHABLE_KEY = $STRIPE_KEY
GOOGLE_CLIENT_ID = $GOOGLE_ID
MAPS_API_KEY = $MAPS_KEY
EOF
echo "Secrets xcconfig generated"
7.4 Test Plans — Advanced Test Configuration
Test Plans (introduced in Xcode 11) are JSON files that define how your tests run.
Creating a Test Plan:
- In your scheme editor, click Test in the left sidebar
- Click Convert to use Test Plans...
- Xcode creates a
.xctestplanfile — commit this to Git - You can create multiple test plans (e.g.,
AllTests.xctestplan,SmokeTests.xctestplan)
Test Plans can configure:
- Which tests to include/exclude
- Environment variables and launch arguments per test run
- Language and region (run all tests in English, then in Arabic)
- Code coverage collection settings
- Test repetition (run each test N times to catch flakiness)
7.5 Multi-Target Widget Setup
Key Rules for Extension Targets:
- Same Team, different Bundle ID: Extension bundle IDs must start with the main app bundle ID. E.g.,
com.yourcompany.myapp.debug.widget - App Groups: To share data between the app and widget, both must be in the same App Group. The App Group ID changes per configuration too
- Same xcconfig inheritance: Create
Widget-Debug.xcconfig,Widget-Staging.xcconfig,Widget-Release.xcconfig
// Configurations/Debug/Widget-Debug.xcconfig
#include "Debug.xcconfig"
// Widget has its own bundle ID (child of main app)
PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.myapp.debug.widget
// Must match the App Group set up in the main app
APP_GROUP_ID = group.com.yourcompany.myapp.debug
// Same API settings as main app
API_BASE_URL = https://dev.api.myapp.com
// In Swift — accessing the App Group container
let groupID: String
#if DEBUG
groupID = "group.com.yourcompany.myapp.debug"
#elseif STAGING
groupID = "group.com.yourcompany.myapp.staging"
#else
groupID = "group.com.yourcompany.myapp"
#endif
let sharedDefaults = UserDefaults(suiteName: groupID)
Chapter 8: Common Mistakes & Troubleshooting
8.1 The "Wrong Environment" Bug
Symptom: App is connecting to the wrong server or using wrong API keys.
| Cause | Fix |
|---|---|
| Running the wrong scheme | Check the scheme selector in the Xcode toolbar |
| xcconfig file not assigned | Go to Project > Info > Configurations and verify each config has the correct xcconfig assigned |
$(inherited) missing |
Add $(inherited) to OTHER_LDFLAGS, HEADER_SEARCH_PATHS, etc. |
| Build Settings GUI override | Clear the GUI setting and let xcconfig handle it |
Info.plist not using $(VAR) |
If Info.plist has a hardcoded value instead of $(API_BASE_URL), the variable substitution never happens |
8.2 Build Errors After Adding xcconfig
Symptom: Build fails with linker errors or framework not found.
| Cause | Fix |
|---|---|
| CocoaPods xcconfig overwritten | Use the #include approach described in Chapter 7.1 |
Missing $(inherited) in xcconfig |
Add $(inherited) to all search path settings |
Circular #include |
File A includes File B which includes File A. Restructure to avoid cycles. |
File not found in #include |
The path is relative to the xcconfig file itself. Double-check the relative path. |
8.3 Schemes Not Showing in CI
Symptom: Xcode Cloud, GitHub Actions, or Fastlane cannot find your scheme.
Cause: The scheme is Personal (not Shared), so it was never committed to Git.
Fix:
- In Xcode, go to Manage Schemes
- Check the Shared checkbox for each scheme
- Commit the new files to Git:
git add MyApp.xcodeproj/xcshareddata/xcschemes/
git commit -m "Make schemes shared for CI"
git push
8.4 Build Setting Not Being Applied
Symptom: You set API_BASE_URL in xcconfig but $(API_BASE_URL) in Info.plist resolves to an empty string.
Diagnosis Steps:
- Select your target, go to Build Settings
- Search for your setting name (
API_BASE_URL) - If it shows the correct value → the xcconfig is working
- If the setting does NOT appear → the xcconfig is not being loaded. Check Project > Info > Configurations
- If the value is empty in one configuration but set in another → your xcconfig for that configuration is missing the key or has a typo
- Click the Levels button to see exactly which level each setting comes from
8.5 Debugging Build Settings Like a Pro
# See ALL resolved build settings for a specific scheme/configuration
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp-Production \
-configuration Release \
-showBuildSettings 2>/dev/null | grep -E "(API_BASE_URL|BUNDLE_ID|ANALYTICS)"
# Output will show:
# API_BASE_URL = https://api.myapp.com
# ANALYTICS_KEY = prod_amp_key_ghi
# PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.myapp
Chapter 9: Fastlane & CI/CD Integration
9.1 How Fastlane Uses Schemes and Configurations
# Fastfile
# Build and upload to TestFlight (Staging)
lane :staging do
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp-Staging", # ← Your Scheme
configuration: "Staging", # ← Your Configuration
export_method: "ad-hoc",
output_directory: "./build",
output_name: "MyApp-Staging.ipa"
)
upload_to_testflight(
skip_waiting_for_build_processing: true
)
end
# Build and submit to App Store (Production)
lane :production do
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp-Production",
configuration: "Release",
export_method: "app-store"
)
upload_to_app_store(skip_screenshots: true)
end
# Run tests
lane :test do
run_tests(
workspace: "MyApp.xcworkspace",
scheme: "MyApp-Dev",
devices: ["iPhone 15 Pro"],
code_coverage: true
)
end
9.2 GitHub Actions Workflow
# .github/workflows/staging.yml
name: Build and Deploy Staging
on:
push:
branches: [develop]
jobs:
build:
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate Secrets xcconfig
run: |
cat > Configurations/Secrets/Secrets-Staging.xcconfig << EOF
STRIPE_KEY = $
MAPS_KEY = $
EOF
- name: Install dependencies
run: bundle exec pod install
- name: Run Tests
run: |
xcodebuild test \
-workspace MyApp.xcworkspace \
-scheme MyApp-Dev \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 15'
- name: Build for Staging
run: bundle exec fastlane staging
Chapter 10: Quick Reference Cheat Sheet
10.1 Complete Build Settings Reference
| Setting Key | Debug | Staging | Release |
|---|---|---|---|
SWIFT_OPTIMIZATION_LEVEL |
-Onone |
-O |
-O |
GCC_OPTIMIZATION_LEVEL |
0 |
s |
s |
DEBUG_INFORMATION_FORMAT |
dwarf |
dwarf-with-dsym |
dwarf-with-dsym |
ENABLE_TESTABILITY |
YES |
NO |
NO |
ENABLE_NS_ASSERTIONS |
YES |
YES |
NO |
VALIDATE_PRODUCT |
NO |
YES |
YES |
ONLY_ACTIVE_ARCH |
YES |
NO |
NO |
SWIFT_ACTIVE_COMPILATION_CONDITIONS |
DEBUG |
STAGING |
(empty) |
GCC_PREPROCESSOR_DEFINITIONS |
DEBUG=1 $(inherited) |
$(inherited) |
$(inherited) |
MTL_ENABLE_DEBUG_INFO |
INCLUDE_SOURCE |
NO |
NO |
SWIFT_COMPILATION_MODE |
(default) | (default) | wholemodule |
COPY_PHASE_STRIP |
NO |
NO |
YES |
STRIP_INSTALLED_PRODUCT |
NO |
YES |
YES |
10.2 Info.plist Variables Reference
| Info.plist Key | Build Setting Variable |
|---|---|
CFBundleDisplayName |
$(APP_DISPLAY_NAME) |
CFBundleIdentifier |
$(PRODUCT_BUNDLE_IDENTIFIER) |
CFBundleVersion |
$(CURRENT_PROJECT_VERSION) |
CFBundleShortVersionString |
$(MARKETING_VERSION) |
APIBaseURL |
$(API_BASE_URL) |
AnalyticsKey |
$(ANALYTICS_KEY) |
EnableCrashReporting |
$(ENABLE_CRASH_REPORTING) |
10.3 xcconfig Syntax Quick Reference
// Comment
// Simple assignment
KEY = value
// Include another file (relative path from THIS file's location)
#include "../Base/Shared.xcconfig"
// Reference another build setting
BUNDLE_ID = com.company.$(PRODUCT_NAME)
// Inherit and append (CRITICAL for CocoaPods compatibility)
OTHER_LDFLAGS = -ObjC $(inherited)
// Platform-conditional
SETTING[sdk=iphonesimulator*] = value_for_simulator
SETTING[sdk=iphoneos*] = value_for_device
// Architecture-conditional
SETTING[arch=arm64] = arm64_value
10.4 The Complete Mental Model
PROJECT
├── Configurations: Debug, Staging, Release (3 named build environments)
└── Targets: MyApp, MyAppTests, MyAppWidget (3 products to build)
Each Target × Each Configuration = a unique set of Build Settings
→ MyApp × Debug = uses App-Debug.xcconfig
→ MyApp × Staging = uses App-Staging.xcconfig
→ MyApp × Release = uses App-Release.xcconfig
Schemes = workflows that say:
→ "When I press Run, build MyApp using Debug configuration"
→ "When I press Archive, build MyApp using Release configuration"
The data flow is:
xcconfig file → Build Settings → Info.plist → Swift code
10.5 Checklist for New Projects
- [ ] Create Xcode project
- [ ] Add Staging configuration (duplicate Release)
- [ ] Create
Configurations/folder structure - [ ] Create 8 xcconfig files with correct content and includes
- [ ] Assign xcconfig files to configurations in Project > Info
- [ ] Add user-defined build settings (
API_BASE_URL,ANALYTICS_KEY, etc.) - [ ] Update
Info.plistto use$(VARIABLE)syntax - [ ] Add
SWIFT_ACTIVE_COMPILATION_CONDITIONS(DEBUG/STAGING) - [ ] Create
AppConfiguration.swiftsingleton - [ ] Create 3 schemes (Dev, Staging, Production)
- [ ] Configure each scheme's Run/Test/Archive build configurations
- [ ] Mark schemes as Shared
- [ ] Add xcconfig secrets files to
.gitignore - [ ] Create secrets xcconfig templates for developers to copy
- [ ] Verify with:
xcodebuild -showBuildSettings - [ ] Commit: xcconfig files, schemes, Info.plist, AppConfiguration.swift
You now have a production-level iOS project setup. One codebase. Three environments. Zero manual switching.