In absence of preprocessor macros, is there a way to define practical scheme specific flags at project level in Xcode project
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Yes, but the best answer is usually not "put flags on the scheme itself." In Xcode, schemes decide what to run, test, archive, or profile, while build configurations and .xcconfig files are the better place for compile-time or configuration-specific values.
So if you want practical scheme-specific behavior without preprocessor macros, the usual approach is to map each scheme to a build configuration and then define settings, plist values, or environment variables for that configuration.
Understand the Scheme vs. Configuration Split
A scheme is a wrapper around actions such as Run, Test, and Archive. A build configuration is a named collection of build settings such as Debug, Release, or custom variants like Staging.
That distinction matters because many flags people call "scheme-specific" are really configuration-specific.
A common setup looks like this:
- scheme
MyApp-Devuses build configurationDebug-Dev - scheme
MyApp-Staginguses build configurationDebug-Staging - scheme
MyApp-Produses build configurationRelease-Prod
Once you structure the project that way, you can attach values cleanly without relying on preprocessor macros.
Use User-Defined Build Settings
Xcode lets you define custom build settings at the project or target level.
For example, create a user-defined setting such as API_ENVIRONMENT and give it a different value in each build configuration.
Then reference it from other settings or from the Info.plist.
A common pattern is to expose it through the plist:
And read it at runtime:
This works well for endpoints, feature switches, logging modes, and analytics toggles.
.xcconfig Files Scale Better
Once a project has more than a couple of values, .xcconfig files are easier to maintain than editing raw build settings in the Xcode UI.
Example Debug-Dev.xcconfig:
Example Release-Prod.xcconfig:
Attach those config files to the matching build configurations, and the scheme picks them up automatically by selecting that configuration.
This is usually the most practical replacement for preprocessor-style branching in modern Xcode projects.
Use Environment Variables for Run-Time Only Values
If the value is only needed while running from Xcode and not in archived builds, scheme environment variables are useful.
In Edit Scheme under the Run action, define environment variables and read them using ProcessInfo.
This is helpful for local developer overrides, but it is not a substitute for build configuration settings because it does not naturally carry into production builds.
Swift Compile Conditions Are Another Option
If the goal is conditional compilation in Swift, use SWIFT_ACTIVE_COMPILATION_CONDITIONS instead of C-style preprocessor macros.
For example, set a value such as STAGING in one build configuration and then use it in code:
This is often the right choice when you truly need compile-time branching in Swift.
Common Pitfalls
The biggest mistake is trying to force schemes to hold what should really be build configuration values. Schemes choose actions, while configurations hold build settings.
Another common problem is using scheme environment variables for values that must exist in archived or distributed builds. Those variables are run-action specific and easy to forget outside local development.
People also scatter settings across the Xcode UI until the project becomes hard to reason about. .xcconfig files are usually much easier to review and maintain.
Finally, if you need compile-time checks in Swift, do not reach for old C-style macros first. SWIFT_ACTIVE_COMPILATION_CONDITIONS is the native tool for that job.
Summary
- Scheme-specific behavior is usually best implemented through build configurations.
- User-defined build settings and Info.plist substitution are practical macro-free tools.
- '
.xcconfigfiles scale well for multiple environments.' - Scheme environment variables are useful for local run-time overrides.
- Use
SWIFT_ACTIVE_COMPILATION_CONDITIONSwhen you need compile-time branching in Swift. - The cleanest setup is to map each scheme to a configuration with explicit settings.

