Migrating from Preprocessor Macros to Custom Flags

12 Sep 2016 2 min read

For years, I've used Preprocessor Macros in Build Settings as a convenient place to store configuration-specific details - this allowed different schemes to have different settings without having to make any code changes. I often applied this to the base URL for accessing the API or ensuring that I was sending analytical events to the correct Mixpanel project. Using Preprocessor Macros resulted in simple code, such as:

[WBSConfig sharedInstance].APIHost = BASE_API_URL;

Rather than:

#ifdef RELEASE
[WBSConfig sharedInstance].APIHost = "https://platform.example.com";
#else
[WBSConfig sharedInstance].APIHost = "https://staging.platform.example.com";
#endif

Or even worse, having a developer manually change the value 😱.

Photo of a toolbox

The end of youthfulness

Sadly, the joyfulness of our youth came to an end because unbeknownst to us, Apple had macros in its crosshairs, and when Swift came along, support for macros wasn't part of it 😫. However, after digging around in Build Settings, I discovered new settings that are similar to Preprocessor Macros:

Swift Compiler - Custom Flags

With Swift Compiler - Custom Flags, we can add our own flags, and these flags act as lightweight Preprocessor Macros. These custom flags are treated as booleans; either they exist or not - assigning a value to them will have no effect, as it's not possible to retrieve that value (in fact, adding a value will result in the compiler treating that flag as if it doesn't exist). When declaring a custom flag, we need to prefix it with -D, so if we want to add a release flag, it would be -DRELEASE, and then to use it, we remove the -D, so RELEASE.

To recap, we can detect if a flag exists based on the configuration of our scheme; however, we can't assign a value to it. To get the same functionality as we used to with Preprocessor Macros, we need to combine these flags with some conditional logic :

class AppEnvironment: NSObject {

    // MARK: Networking
    
    class func clientID() -> String {
        var clientID: String?
        
        #if DEBUG
            clientID = "a5b0fb978fad9588af608c06382d45ee5396a29eb12f8b6bbec260569aebe45c"
        #elseif RELEASE
            clientID = "8ca5b0fb978fad06382d49e5396a29eb588af605e12f8b6bbec260569aebecc7"
        #endif
        
        assert(clientID != nil, "Client ID not set")
        
        return clientID!
    }
    
    class func clientSecret() -> String {
        var clientSecret: String?
        
        #if DEBUG
            clientSecret = "cd0cd93fe55c51007a45782de93c48c157de5f7f907267593309eea7d4c9064c"
        #elseif RELEASE
            clientSecret = "64cde93c48c157d2759330c51007a4578c90e5f7f99eea7d4cd0cd93fe550726"
        #endif
        
        assert(clientSecret != nil, "Client secret not set")
        
        return clientSecret!
    }
    
    class func baseAPIURL() -> String {
        var baseAPIURL: String?
        
        #if DEBUG
            baseAPIURL = "https://development.platform.example.com"
        #elseif RELEASE
            baseAPIURL = "https://platform.example.com"
        #endif
        
        assert(baseAPIURL != nil, "Base API URL not set")
        
        return baseAPIURL!
    }
    
    // MARK: Analytics
    
    class func mixpanelAppToken() -> String {
        var mixpanelAppToken: String?
        
        #if DEBUG
            mixpanelAppToken = "a1278c97bb9d0e1034032011ca4a547c"
        #elseif RELEASE
            mixpanelAppToken = "32011ca4a547c7bba1278c903409d0e1"
        #endif
        
        assert(mixpanelAppToken != nil, "Mixpanel app token not set")
        
        return mixpanelAppToken!
    }
    
    class func crashlyticsAPIKey() -> String {
        var crashlyticsAPIKey: String?
        
        #if DEBUG
            crashlyticsAPIKey = "2e29b9629b2ff220ec706d264cafcf42fcd05abb"
        #elseif RELEASE
            crashlyticsAPIKey = "cf42d05ab2fd264cafb220ec706fc9b2e29b962f"
        #endif
        
        assert(crashlyticsAPIKey != nil, "Crashlytics API key not set")
        
        return crashlyticsAPIKey!
    }
}

In the above code snippet, we have encapsulated access to the flags behind a Swift interface so the rest of the app calls a class method on the AppEnvironment class to get a string back without having to pass in any state.

It's not as clean a solution as Preprocessor Macros, however, it still prevents the need to manually edit source files to change environment settings and helps to make these values a little less magic.

To see the above code snippets together in a working example, head over to the repository and clone the project.

What do you think? Let me know by getting in touch on Mastodon or Bluesky.