A robust multi-environment build setup

Sam Dods
By Sam Dods under Engineering 22 April 2016

Almost all projects have a need for multiple environments. This allows us to test our user-facing software against non-production data, and deploy in-development instances of our backend for testing purposes.

We usually set up our projects to use multiple environments by having different build-time configurations for each. Sometimes there is also a need to adjust the look and feel of an app for different versions, which we refer to as skinning or theming. We would usually achieve this also at build-time, by injecting theme files that contain colours, fonts, brand images, etc. However, that's out of scope of this article.​

In this article, I'm going to discuss how we currently support multiple environments in our iOS apps. We've arrived at this solution after several previous attempts failed to meet our requirements for robustness and reliability. I will explain the disadvantages of other solutions, and I'll highlight how our robust alternative holds up where others fail.

Some examples of settings you might wish to define differently per environment are API keys for third-party libraries; or different URLs for particular services.

If you build it, they will come

The most common alternative approach

A common solution to supporting multiple environments is to use Xcode Configurations and Schemes. This is the recommended approach from the top search results on the subject.

An Xcode Scheme is created for each environment (e.g. Development, Test, UAT, Staging, Production). Each Scheme then uses a different Build Configuration. Every single Build Setting can be configured differently for each Configuration - including Architectures; Base SDK; whether On Demand Resources are enabled; whether Bitcode is enabled; which Info.plist to use; which Provisioning Profile to use; or what the Bundle Identifier will be.

Any of these settings, and a whole host of others, can be changed per Configuration. In doing so, you run the risk of – at best – not being able to install the app on your device after archiving it and distributing an in-house build. At worst, you run the risk of releasing to the App Store with unwanted settings.

Also, if you have a legitimate reason to change a particular setting for all of your "release" environments, then you need to change it in multiple places.

One example of an unwanted setting reaching the App Store is the Supported Interface Orientations, defined in the Info.plist. The app could go through all levels of testing, with videos verified as working correctly in landscape. But then you come to install the version you released to the App Store and it does not support playing video in landscape. This is because a different Info.plist was used during testing.

So really, we want to ensure that the Xcode Build Settings we are testing against are the exact same settings that we release the app with. ​ A clear advantage to this approach is that it is simple to change environment during development by changing the Build Scheme in Xcode.

Common safeguards still have their disadvantages

Developers would usually limit what they change per Configuration to a list of User-Defined Build Settings (although there is no way to enforce this restriction).

User-Defined Build Settings can be referenced from the Info.plist. This way you don't have to create a separate Info.plist for each environment, you would have a single default Info.plist from which you reference your custom settings. Plus you can define custom settings for each environment supported.

However, because Xcode does not defend against changing other Build Settings, you still risk encountering the issues described above.

Another disadvantage to User-Defined Build Settings is that they are all defined as strings, with no validation of what has been input. Also, because all of your settings are entered as strings, you need to convert them to the data type you're actually interested in, which might be a Boolean or a URL, and we need to write repetitive code to convert each setting.

So you might have a setting that contains the URL for the main entry point into your backend. If this is an invalid URL that cannot be parsed to create an NSURL instance, then you won't find out until you run the app.

Wouldn't it be nice to know about this error when compiling?

Another alternative

There are various other solutions to the multi-environment problem. ​ One of which is to use GCC pre-processor macros. However, this is a messy approach which usually requires defining macros in a Configuration Settings File (a.k.a. .xcconfig) for development, and passing as command line arguments from a build script for release.

It's a little messy to set up, but can be done without the need for multiple Configurations. This means that you can do all of your development on the default "Debug" Configuration, and carry out all levels of testing against the "Release" Configuration, by injecting the pre-processor macro based settings at compile time.

A major disadvantage to this approach is that in development, we change environments by commenting out in our Configuration Settings File. However, this is not an ideal way to change configuration.

Other disadvantages

The solutions described above all require you to develop some kind of AppEnvironment class (or struct) to read the settings from the Info.plist or from another Plist or flat file, or to create settings from the pre-processor macros. And all implementations of such class that I've seen have contained other logic to convert from strings into various data types, including URLs. Ultimately, we want a solution that does not require this component to be implemented manually.

Every time you add a new setting, you need to add it to your property list or flat file, and manually edit the code of your (e.g.) AppEnvironment file to add a property to wrap/hide the access to the property list, such as:

static var analyticsKey: String {
  let key = "analyticsKey"
  return plistDict[key]
}

And you know the worst thing?

You have to redefine, in code, the string to use as a key to the value in the property list.

Building a robust solution

So before describing a solution, let's look at our requirements: ​

The solution that we've come up with meets all of these requirements. It's simple to setup, and we've found it to be super robust and reliable in a number of products for some time now, with simple extensibility.

Automatic Configuration Generator

We've created a simple tool – called configen for now – which is used to create an environment configuration class at build time. It has the following characteristics:

The tool is written as a Swift command line script, so it can validate property data types, including the conversion of a string to an NSURL instance. And the mapping file uses a format that feels familiar to Swift developers.

The mapping file:

entryPointURL : NSURL
searchURL : NSURL
retryCount : Int
adUnitPrefix : String
pushKey : String
analyticsKey: String

On the left you provide the name of the static property to implement in your environment configuration class. This is also the key in the property list file. After the colon, on the right, you specify the type of the property as it will be implemented.

The property list file:

enter image description here

The output:

// auto-generated by configen

import Foundation

class AppEnvironment {
  static let entryPointURL = NSURL(string: "http://dev.theappbusiness.com/mediamate/entry")!
  static let searchURL = NSURL(string: "http://dev.theappbusiness.com/mediamate/search")!
  static let retrycount = 3
  static let adUnitPrefix = "/1234/configen/"
  static let pushKey = "XYZCONFIGENTAB123vHreq"
  static let analyticsKey = "UA-0123565-99"
}

If there are properties defined in the mapping file that do not exist in the property list, or if the types don't match up, then the script will produce an error.

The configen script:

The script itself is available here, with full instructions for how to use it, and how it meets all of the requirements we set out for it.

Conclusion

We now have a robust solution for a multiple environment build setup. It might seem a little more difficult to set up in the first place, but once it's in place, it can provide a lot of efficiencies in terms of time spent working with multiple configurations. Especially in terms of time saved due to related bugs!

Also, the additional setup is counteracted by the fact that you don't need to implement your own class as an interface to your configuration file. All in all, providing less opportunity for human error!

Thanks for reading. If you want to join us creating robust and reliable apps, why not get in touch.