Customising apps based on scheme

Application schemes are commonly used to create different versions of applications. That's why we create them in the first place. Good example might be a white label app that has schemes corresponding to different brands. Another common pattern is having scheme for each application environment. In general we want to tweak apps based on selected scheme. Today we'll talk about how neatly make customisations in code.

From scheme to code

Unfortunately we cannot directly check in code which scheme is used. However, each scheme can use different build configuration and in each build configuration we can specify unique flag, which can be referenced through code via compiler directives. We can utilise this mechanisms to alter flow or data based on whether given flag, and therefore scheme, is set or not.

If you run your app from Xcode with default build configuration, it will have DEBUG flag raised, though if you were to archive the application it won't be set. In consequence, common practice is to wrap print statements in #if DEBUG.

func fetchUserToken() -> String? {
    let token = persister.retrieveToken()
    #if DEBUG
    print("Found token: \(token)")
    #endif
    guard token.isValid else { return nil }
    return token
}

That way print("Found token: \(token)") statement won't get into released application, but will print useful debugging information when run from Xcode. Same codebase, but different behaviour.

Connecting to different environments

For the sake of example imagine 2 backend environments — development and production, that our app connects to. Our app will have 2 versions (schemes) as well. Using the same technique we can connect to appropriate backend environment based on selected scheme.

Let's create Development scheme along with Development build configuration and set DEVELOPMENT flag in it. Don't forget to tell newly created scheme to use this build configuration for every action. To set the flag look for Active Compilation Conditions build setting in Swift Compiler - Custom Flags section. With that in place, it's matter of checking whether the flag is raised or not:

#if DEVELOPMENT
let URL = URL(string: "https://dev.jakub.codes/api/")!
#else
let URL = URL(string: "https://jakub.codes/api")!
#endif

Done. While it's way better than commenting out one variable and uncommenting the other one, it has its flaws.

...but better

Scheme of application is really its state. Swift gives us beautiful way to express finite states — enums. Let's create one to represent schemes of our app:

enum AppScheme {

    case development
    case production
}

Let's set flag in each build settings named like scheme and add static property current, so that we could change values and flow based on it anywhere in codebase:

// Current scheme of the application
static var current: AppScheme {
    #if DEVELOPMENT
        return .development
    #elseif PRODUCTION
        return .production
    #endif
}

Thanks to having separate flag for each scheme and using #elseif instead of #else, we can guarantee that we won't forget to specify flag in build settings or make a typo in it — if we do, it won't compile. With new implementation, we can greatly improve how we implemented different URLs for different schemes:

var url: URL {
    switch AppScheme.current {
    case .development:
        return URL(string: "https://dev.jakub.codes/api/")!
    case .production:
        return URL(string: "https://jakub.codes/api/")!
    }
}

We've addressed all flaws of initial solution:

Taking it even one step further, based on John Sundell's tip, we can create an extension to URL that allows us to instantiate instance of URLs from static string literals.

extension URL: ExpressibleByStringLiteral {

    init(stringLiteral value: StaticString) {
        guard let url = URL(string: "\(value)") else {
            fatalError("Invalid URL string literal.")
        }
        self = url
    }
}
var url: URL {
    switch AppScheme.current {
    case .development:
        return "https://dev.jakub.codes/api/"
    case .production:
        return "https://jakub.codes/api/"
    }
}

Swifty! Let's revisit first example. Now, we can express the same logic using simple if in just 1 line.

func fetchUserToken() -> String? {
    let token = persister.retrieveToken()
    if AppScheme.current == .development { print("Found token: \(token)") }
    guard token.isValid else { return nil }
    return token
}

I hope this neat way of customising application between different schemes will improve your codebase. If you don't feel comfortable with schemes, targets, build settings and so on stay tuned for future articles.


twitter logoDo you find this article insightful? Let others know!