Unleashing your build settings

September 12th, 2016
#xcode

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. This 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 changing the string 😱 (yes, I've actually seen this in production on a banking app).

The end of youthfulness

Sadly the joyfulness of our youth came to an end because unbeknown to us Apple had macros in its crosshairs and when Swift came along support for macros wasn't part of it 😫. However after digging about 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 with it -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.

Ok, so we can now detect if a flag exists based on the configuration of our scheme however we can't assign a value to it. We need to combine these flags with some conditional programming to get the same result as we had before (similar to a macro approach we looked at, and discarded, above).

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 just calls a class method on the AppEnvironment class and gets 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 actually helps to make these values a little less magic in nature (I think we are all agreed we should avoid magic strings and numbers if at all possible).

You can find the completed project by heading over to https://github.com/wibosco/UnleashingBuildSettings-Example

What do you think? Let me know by getting in touch on Twitter - @wibosco