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.
- There's no error if flag isn't set
- If we add new scheme we aren't forced to decide to which env it should connect
- We are not sure what
DEVELOPMENT
is referring to or when it is being set - It doesn't look swifty — reminds me of
#define
and#ifndef
from C++
...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:
- one can instantly see that URL depends on current application scheme.
- if future you, decides to add new scheme
switch
will force you to decide to which backend environment app should connect - we made it typo proof, as it won't compile if there's one
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.