• Best strategies for configuring Multiple environments in Xcode projects
  • Translator: Bruce

What have you configured for different environments? You might have a debug only view, or you might want to turn off logging for the release. You may have multiple backend environments that can be configured for dev, QA, UAT, stage, PROd, and so on. Each of these requires a different root URL, API key, and app secret. The application may also integrate with social media, crash reporting tools or other analytics tools, and we should not contaminate this data through our testing efforts. We may also want to change the application icon and application name to show the environment in which the installed application is running.

Developing simple iOS apps is easy without worrying about too much configuration. When you’re just getting started, you can use the code to make some Settings, changing the values as needed. You can even try commenting/uncommenting lines of code to switch between configurations. Some people use #if DEBUG. Any one of these could quickly become a problem. It is error-prone and time-consuming. So what do we do?

Let’s look at an example of setting up a project with multiple environments. I only know how to do two, but I can repeat the steps as needed.

To establish

You can use an existing project or create a new one. A single view application is appropriate for this demonstration. I call it “EnvironmentsTest”. By default, you will have a scheme and two configurations (debug and release). Let’s start with a structure to hold our configurable properties.

struct Config {
    let scheme: String
    let host: String
    let key: String
    
    init() {
        scheme = "https"
        host = "api.testapp.com"
        key = "key.testapp.prod"}}extension Config {
    static var current: Config = Config()}Copy the code

To test this, we can print the configuration when the application finishes launching. Note: Don’t forget to remove it; you don’t want your application to leak this information in production.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        
    print(Config.current)
    return true
}
Copy the code

In the console, you will see the desired configuration printed.

Config(scheme: "https", host: "api.testapp.com", key: "key.testapp.prod")
Copy the code

Debug & Release vs. Dev & Prod

When you only have two environments to worry about, it’s almost natural to assign debugging to DEV and release it to PROD. I’ve seen a lot of them. Unfortunately, it doesn’t work. Dev and PROd represent environment configurations, while Debug and Release are build configurations. You should be able to mix and match them to some extent. For example, you might want to debug a PROD build. You will never release a development build, but you might want to analyze a development build, and we usually use a release configuration. Of course, when you try to add more environments, you will completely crash.

The configuration is injected via info.plist

We can replace the default configuration by creating readable entries in the info.plist file.

<key>Config</key>
	<dict>
		<key>scheme</key>
		<string>http</string>
		<key>host</key>
		<string>dev-api.testapp.com</string>
		<key>key</key>
		<string>key.testapp.dev</string>
	</dict>
Copy the code

Then change the Config extension to read it. It might be tempting to go straight to infoDictionary, especially if we only have three values, but as your configuration becomes more complex, it’s better to have something that can be decoded.

struct ConfigContainer: Decodable {
    let Config: Config
}

extension Config: Decodable {
    static var current: Config = {
        guard let infoURL = Bundle.main.url(forResource: "Info", withExtension: "plist") else { fatalError("No info.plist in main bundle")}do {
            let infoData = try Data(contentsOf: infoURL)
            let decoder = PropertyListDecoder(a)let item = try decoder.decode(ConfigContainer.self, from: infoData)
            return item.Config
        } catch {
            return Config() ()}}}Copy the code

Now we can see the configuration of the development environment.

Config(scheme: "http", host: "dev-api.testapp.com", key: "key.testapp.dev")
Copy the code

So, now we inject configuration through info.plist, which is a step in the right direction. Now, how do you switch between these environments? We need a way to define all configurations and then be able to choose between them.

Targets

When developers want to install multiple versions of applications (in different environments) on the same device, they often add Target. In most cases, adding a target is redundant. When adding targets, you must remember to set the Settings for each target. For example, if camera access is to be enabled in the application, the required info.plist must be set separately for each target. Also, whenever a file is added to a project, you must ensure that it is added correctly to each target.

What we’re really looking for is to select the environment/configuration from the drop down list and run it. Schemes are a good one. First, we need to set up some build configurations.

Build Configurations

We start with the Debug and Release configurations. Let’s integrate this with our production environment. Simply rename Debug to prod-DEBUG and publish it to prod-Release. Next, create a new configuration and copy prod-Debug. Rename the new configuration dev-debug.

In most cases, you do not need to copy the release. Performing debugging and release for each environment becomes confusing and difficult to maintain. I recommend adding publishing configurations only for the environments you want to analyze or distribute.

Schemes

Now that the build configuration is set up, we can create a scheme. Click the Scheme drop – down list, and then select Edit Scheme…

Select the ** “Duplicate Scheme” ** button in the lower left corner. Name the new schema to indicate the target and environment; I call it “environmentstest-dev”.

Make sure shared is selected for Scheme. Even if you are not working with the team, you can be sure to save the Settings when you move to another computer. For each type of Build **(Run, Test, Profile, Analyze)** on the left, select the Info TAB and set Build Configuration to dev-Debug. If you create a publish configuration for this environment, you should use it for profiles. The consequences of profiling performance in a Debug configuration are beyond the scope of this article, and in some cases can be, except that the results may be different in Release. I always set Archive to prod-release. In this way, no matter which solution is chosen, the Archive build will be built for Prod. Then I don’t have to worry about accidentally uploading the Dev version. Ultimately, it’s best to rely on other workflow tools to prevent such errors, but that’s a topic for another day.

Press the close button when finished.

At this point, we added build configurations and Scheme to build for different environments. However, if you run the application, each environment will get the same results. How do we actually customize it?

Configuration Settings file

The configuration Settings file is a good place to set up each environment, configuration Settings. Go to File -> New -> File… Or by ⌘ N. Scroll down to the “Other” section and select Configuration Settings File. Press Next and name them dev.xcconfig and Create. Enter or copy the following:

scheme = http
host = dev-api.testapp.com
key = key.testapp.dev
Copy the code

Then go back to the info.plist file and replace the Config section with the following.

<key>Config</key>
	<dict>
		<key>scheme</key>
		<string>$(scheme)</string>
		<key>host</key>
		<string>$(host)</string>
		<key>key</key>
		<string>$(key)</string>
	</dict>
Copy the code

Finally, return to the location where the project configuration was created. You will notice that the right column is Based on Configuration File. Modify the dev-debug project to use the Dev configuration.

Now, when you run the project with the Dev scheme, you will see the result of the Dev configuration, and when you run the project with the Prod scheme, you will see an empty configuration. So, we should create another configuration Settings file for the Prod build. However, due to exposure, I don’t like this solution. I suggest we approach ProD differently.

Prod config

Info.plist is not the right place to put any application secrets if you are distributing them to the public. Therefore, we need to find another way to configure Prod. Actually securing these values is beyond the scope of this article, but a good place to start is to remove them from info.plist and convert them into compiled code. If you look back at our Config structure, it already exists. All we need to do is remove the (empty) value from info.plist and the code should load our default configuration, which is Prod.

Select the Project from the Project Navigator, select the target, and go to the Build Phases TAB. Click the **+ button, then select “New Run Script Phase” **. Click on the title to rename it to a less generic name. I set the title to “Remove Prod Config” and copied the following into the script area.

PLISTBUDDY="/usr/libexec/PlistBuddy"
INFO_PLIST="${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
if [[ -z ${host} ]]; then
$PLISTBUDDY -c "Delete :Config" "${INFO_PLIST}" || true
fi
Copy the code

That is, if the “host” variable is empty (or nonexistent), the Config object is removed from the info.plist file.

Note: You must run **clean Build (⌘⇧K) ** to force the build phase to run.

Now you’ll get the correct results in each scheme! 🎉

App Transport Security

As you may be aware, Apple now requires applications to support best practice HTTPS security. It’s usually a good idea to follow this rule for every environment, even in development. However, you may not always do this (for example, when preparing a “local” environment) or you may not be configured yet, but you still need to keep developing. We can handle this with another Run Script Phase. Name this “HTTP-allows Arbitrary Loads” and copy the following into the script area.

PLISTBUDDY="/usr/libexec/PlistBuddy"
INFO_PLIST="${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
if [ ${scheme} == "http" ]; then
$PLISTBUDDY -c "Add :NSAppTransportSecurity dict" "${INFO_PLIST}"
$PLISTBUDDY -c "Add :NSAppTransportSecurity:NSAllowsArbitraryLoads bool true" "${INFO_PLIST}" || true
$PLISTBUDDY -c "Set :NSAppTransportSecurity:NSAllowsArbitraryLoads true" "${INFO_PLIST}"
else
$PLISTBUDDY -c "Delete :NSAppTransportSecurity:NSAllowsArbitraryLoads" "${INFO_PLIST}" || true
fi
Copy the code

Through the application (_ : didFinishLaunchingWithOptions:) print the value to test each scheme.

print(String(describing: Bundle.main.infoDictionary?["NSAppTransportSecurity"]))
Copy the code

Pretty good, isn’t it?

Run Dev and Prod in parallel

Sometimes it’s convenient to install Dev build and Prod Build on the same device so you can switch between the two. To do this, all you need to do is change the package ID. You also need a simple way to determine which is which. To do this, we can change the display name and icon.

Note: For all of these Settings, I always keep the production build; This is a build that goes into the App Store and should not be modified. Thanks to this rule, it’s also easy to recognize because it doesn’t have any modifiers.

Bundle Identifier

To change the Bundle Identifier, go to target’s Build Settings and search for “Bundle Identifier”, then click the arrow to expand all configurations. I usually append a “-” followed by the configuration name.

Display Name

These steps are the same as Bundle Identifier, but this time search for “product name”. To do this, I suggest replacing the name with the environment. Otherwise, the text may be too long to use.

Icon

Changing ICONS is almost as easy. On the same screen, search for “Icon”. You should see “Asset Catalog App Icon Set Name.” Again, add a dash followed by the configuration name.

conclusion

Performing all these steps for each configuration may seem like a lot of work to change some Settings, but it’s well worth it. Of course, it takes some time to do all the setup, but when it’s done the switch becomes trivial. Take the time to do the right thing and you’ll save time in the long run. You can save time by switching quickly between environments. You can save yourself the trouble of making manual mistakes.