> ## Content Index
> Fetch the complete content index at: https://williamboles.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Bye-bye Preprocessor Macros. Hello Active Compilation Conditions
- URL: https://williamboles.com/bye-bye-preprocessor-macros-hello-active-compilation-conditions/
- Published: 2016-09-12T15:56:35.000Z
- Updated: 2026-09-26T16:34:31.000Z
- Description: Preprocessor Macros don't work with Swift. Here, we look at how to switch to using Active Compilation Conditions to enable configuration-specific build settings.
- Author: William Boles
- Tags: Xcode

In an Objective-C project, it is not uncommon to open `Build Settings` and see a long list of `Preprocessor Macros`. `Preprocessor Macros` are a feature inherited from C. Before the compiler compiles our code, a separate step - the preprocessor - runs through the source files and performs text substitution, replacing every occurrence of a macro with its defined value. So when we define `API_DOMAIN` as "[https://platform.example.com](https://platform.example.com/?ref=williamboles.com)" in our build settings, the compiler never sees `API_DOMAIN` \- it just sees the raw string. The real power of `Preprocessor Macros` is that each build configuration can have its own list, so we can have a different value for `API_DOMAIN` depending on whether the project was built using the `Debug` or `Release` configuration.

So by using `Preprocessor Macros` we could write code like this:

```
@interface Environment : NSObject

// 1
@property (class, nonatomic, readonly) NSString *apiDomain;

@end

@implementation Environment

// 2
+ (NSString *)apiDomain {
    return @API_DOMAIN;
}

@end
```

Here's what we did above:

1. `apiDomain` is declared as a read-only class property, meaning it's accessed on the class itself rather than on an instance.
2. The getter is manually implemented since class properties aren't auto-synthesised. It returns the `API_DOMAIN` preprocessor macro as an `NSString` literal.

> The `@` before `API_DOMAIN` in the getter converts the C string macro into an `NSString` literal.

Sadly, Swift will never know the joys of `Preprocessor Macros` 😫.

> `Preprocessor Macros` don't fit the design philosophy of Swift - there's no type checking, no scoping, and no namespacing with `Preprocessor Macros`.

![Photo of a control button with lots of switches](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2026/02/photo-of-control-panel-min.jpg)

This post will cover how we can migrate our `Preprocessor Macros` to `Active Compilation Conditions`.

## The Swift Way

`Active Compilation Conditions` are boolean switches resolved at compile time. The compiler evaluates `#if` / `#elseif` / `#else` blocks and only compiles the code in the matching branch - the other branches don't exist in the final binary at all. Like `Preprocessor Macros`, they can be set per configuration; unlike `Preprocessor Macros`, they can't carry a value - they either exist or they don't. Where `Preprocessor Macros` performed text substitution, injecting values directly into our code, `Active Compilation Conditions` simply control which code paths get compiled.

> For new projects, Xcode automatically populates `Active Compilation Conditions` with `DEBUG` for the `Debug` configuration - other configurations don't get pre-populated. If you are migrating from an existing project, Xcode isn't as helpful - you will need to add them to all configurations.

To get the same functionality as we used to with `Preprocessor Macros`, we need to combine these flags with conditional logic to get the values we want:

```
// 1
enum Environment {
    static func apiDomain() -> String {

        // 2
        #if DEBUG
            return "https://development.platform.example.com"
        #elseif RELEASE
            return "https://platform.example.com"
        #else
            // 3
            fatalError("Missing API domain")
        #endif
    }
}
```

1. `Environment` is declared as an enum with no cases, making it act as a pure namespace that can't be instantiated.
2. These preprocessor directives filter out the branches that don't match the current active build configuration.
3. If neither `DEBUG` nor `RELEASE` is set, the app crashes with a descriptive message. We want to fail fast here so that the missing branch is picked up during development.

It's not as clean a solution as `Preprocessor Macros`; however, it's safer, and it still prevents the need to manually edit source files to change environment settings.

> Safer because the compiler checks every branch rather than silently substituting text.

To see the complete working example, visit the [repository](https://github.com/wibosco/UnleashingBuildSettings-Example?ref=williamboles.com) and clone the project.