Xcode Environment Specific Configuration
Almost every app you build with Xcode will need some sort of configuration. Whether it is API Keys for 3rd party SDKs, the URL of your API, feature toggles, or a logging level of verbosity, it’s a good idea to keep this configuration separate from your code.
The biggest reason you would want to do this is so that you can provide a different value depending on your build settings. For development builds, use a development API. For production, use the live API.
You might even want to isolate analytics between your beta distribution and your App Store distribution.
The way I approached this on the NSScreencast app is by using the Info.plist
along with a custom configuration plist that I write a wrapper for.
Defining configuration per environment
In my projects I start by adding a Configuration
group, along with a propertly list called config.plist
.
In the plist there are three root keys:
Common
- These will hold settings that apply no matter the environmentLocal
- These apply when running a specific scheme for local developmentProduction
- This applies to any release build
Aside from Common
you can name the environments whatever you like. You aren’t limited to these either, you can add as many as you need for your project.
How do I specify which environment to use?
The approach I’ve taken for this is to leverage Xcode build configurations along with the Info.plist
. The reason is that the Info.plist
is easy to read from your application and can contain substitutions based on build settings.
We’ll start by making an xcconfig file structure:
Configuration/Shared.xcconfig
Configuration/Debug.xcconfig
Configuration/Debug.LocalServer.xcconfig
Configuration/Release.xcconfig
You’ll want one of these for each build configuration you maintain. Here I’ve added a Shared.xcconfig
in case there are any settings that would otherwise be duplicated in all of the config files.
You can also see that I have an extra one called Debug.LocalServer.xcconfig
. This is because I added an extra build configuration for the sole purpose of changing configuration like this. My build configurations look like this:
What if I use CocoaPods? Doesn’t it already use xcconfig files?
Yep, you’ll have to take one extra step if you already have xcconfig files in use, which is true if you use CocoaPods.
Take note of the xcconfig file that is in use already, then simply include it in your own xcconfig file.
Here’s my Release.xcconfig
:
#include "../Pods/Target Support Files/Pods-NSScreencast-Base-NSScreencast/Pods-NSScreencast-Base-NSScreencast.release.xcconfig"
#include "Shared.xcconfig"
ConfigEnvironment=Production
And here is my Debug.LocalServer.xcconfig
:
#include "../Pods/Target Support Files/Pods-NSScreencast-Base-NSScreencast/Pods-NSScreencast-Base-NSScreencast.release.xcconfig"
#include "Shared.xcconfig"
ConfigEnvironment=LocalServer
All we are doing here is setting a build setting called ConfigEnvironment
.
You can do loads of other things with these xcconfig files, including keeping interesting build settings under version control with comments. If you’re curious, take a look at the screencasts I did on the topic: xcconfig files (and part 2). Also worth mentioning is James Dempsey’s BuildSettingExtractor tool.
Now that we have a different build setting per environment, we need to make this available at runtime. Open the Info.plist
.
We’re going to add one line here at the top level:
This dynamic value gives us the missing piece to be able to determine the configuration at runtime.
Info.plist
. My answer is that this file already has a lot of uses, and is owned primarily by the operating system. I prefer isolating my configuration so that it easy to understand and change. There's also another benefit which I’ll get to at the end of this post.Tying it all together
Now that we have our configuration values, and we know what environment we’re in, we need an easy way to refer to these values at runtime. For this, we’ll create a class called EnvironmentConfiguration
:
final class EnvironmentConfiguration {
private let config: NSDictionary
init(dictionary: NSDictionary) {
config = dictionary
}
convenience init() {
let bundle = Bundle.main
let configPath = bundle.path(forResource: "config", ofType: "plist")!
let config = NSDictionary(contentsOfFile: configPath)!
let dict = NSMutableDictionary()
if let commonConfig = config["Common"] as? [AnyHashable: Any] {
dict.addEntries(from: commonConfig)
}
if let environment = bundle.infoDictionary!["ConfigEnvironment"] as? String {
if let environmentConfig = config[environment] as? [AnyHashable: Any] {
dict.addEntries(from: environmentConfig)
}
}
self.init(dictionary: dict)
}
}
Note that the values in Common
can be overridden by the environment specific values. This way you can provide defaults in the Common
category, and then provide an override just in one environment if you need to.
This parses our plist, but doesn’t provide a way to read the values. This is where I add an extension and provide strongly-typed accessors for each key I want to by able to read:
extension EnvironmentConfiguration {
var baseApiUrl : String {
return config["BaseApiUrl"] as! String
}
var logLevel : String {
return config["LogLevel"] as! String
}
var googleClientId: String {
return config["GoogleClientId"] as! String
}
var oneSignalAppId: String {
return config["OneSignalAppId"] as! String
}
var appCenterKey: String {
return config["AppCenterKey"] as! String
}
}
Doing it this way means I can share the EnvironmentConfiguration.swift
between projects, and then you just add an extension for the keys specific to your application.
Now, when I need a value from configuration, I can do this:
let config = EnvironmentConfiguration()
let apiClient = ApiClient(baseUrl: config.baseUrl)
What about Testing?
I’m glad you asked! With this structure it’s also trivial to initialize with your own in-memory dictionary instead of reading from the file. The only thing to worry about here is to avoid instantiating this configuration object from everywhere in your code. In fact, it may be beneficial to have your view controllers and other components depend on an abstraction:
protocol NSScreencastConfiguration {
var baseApiUrl: String { get }
var logLevel: String { get }
var googleClientId: String { get }
var oneSignalAppId: String { get }
var appCenterKey: String { get }
}
extension EnvironmentConfiguration : NSScreencastConfiguration { }
With this in place, any component in your system that needs configuration should refer to the protocol instead of the concrete type.
class MyViewController : UIViewController {
private let config: NSScreencastConfiguration
init(config: NSScreencastConfiguration) {
self.config = config
super.init(nibName: nil, bundle: nil)
}
required init(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
if config.logLevel == "verbose" {
// log all the things!
}
}
}
Passing around dependencies like this is a difficult practice to get into (and adhere to). It adds friction, but it is a good idea to make your dependencies explicit.
What else can you do?
This post so far describes static configuration. This is configuration that you set at development time and never change. But what if you could modify these values… at runtime?
This requires adding some additional layers to the approach, but essentially is the same underlying idea: Your configuration is stored in a dictionary in memory, and you pass around the configuration object to components that need it.
This means it would be possible for you to build a sort of live debug screen in your debug and beta configurations, allowing you to tweak settings, target a different version of the API, or turn up log levels dynamically.
It would be like having a Quake Console for your app.
Configuration is an important piece of most applications. By providing a flexible structure for providing configuration values, altering them based on environment, you can have a clean separation of code and configuration.
How do you handle configuration in your apps? I’d love to hear about alternative approaches to this concept. Hit me up on Twitter.