A robust and efficient multi-environment build setup for app design Creating a robust multi-environment build setup for app design
Skip to main content

Select your location

App designer main tools laptops and smartphones

Creating a robust multi-environment build setup for app design

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 app design 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.

IfyouXcodebuildit

The most common alternative approach

A common solution to supporting multiple environments is to use Xcode Configurations and Schemes for the app design. 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 app design 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:

Code snippets

Building a robust solution
 

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

  • Trivial switching between environments during development,
  • No risk of changing Build Settings that we don't actually want to be different between environments,
  • Assurance that the Build Settings we test are exactly the same as we release,
  • Build-time error if a setting is missing,
  • Build-time validation of a setting's data types (e.g. URL, Bool, String),
  • No need to convert from a string to the appropriate data type,
  • No need to implement an (e.g.) AppEnvironment class or struct manually,
  • Should be easy to extend with new settings over time.

The solution that we've come up with meets all of these requirements. It's simple to set up, 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:

  • It maps settings from an environment property list to static properties of the environment configuration class,
  • It uses a mapping file to specify the expected type of each setting in the property list, so that the static properties available in your class are of the expected type,
  • It produces a build error if a property is missing or its type is invalid,
  • You can define a different property list per environment.

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:

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 mapping file

The property list file:

The property list file:

The output

The output

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 app design 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!

Interested in learning more?

Get in touch