Dec 22, 2020

Multiple App Environments Using Xcode Schemes

When developing a software product, you'll end up with multiple environments, for your local development environment, one or many development/staging environments, and a production environment (or multiple depending on your architecture). The same applies to mobile applications.

Whether it's for push notifications that use a sandbox when developing, or simply using different backends for each environment (you might want to develop against your local backend but run the production app against the live environment), chances are, that you want to develop and distribute multiple variations of your app, without too much overhead.

Using Xcode schemes, you can do just that.

Creating an Xcode Scheme

Next to the device selection, the current scheme is displayed. If you haven't set up custom schemes, yet, the default scheme (usually the name of your project) will be shown.

You can click on the current scheme, and then select Manage Schemes to open the following overlay

In here, you can see a list of your project's schemes, as well as options on whether the scheme should be shown in Xcode, and whether it should be added to your project files to be checked into version control and shared with your teammates.

Clicking the gear at the bottom of the modal will display useful options for Importing/Exporting schemes, as well as duplicating an existing scheme, which we'll make use of to create our staging scheme, which will point to a staging environment.

For the duplicated scheme, you can simply enter a name, confirm and close the settings.

Environment-specific configurations in Xcode

For common settings like the bundle identifier, app icon, or other build and release settings, Xcode offers schema-based configuration out of the box.

Going into the Build Settings, you can expand any option to set values specific to each environment. This allows you to set custom names, app icons, bundle identifiers, and other environment-specific settings for each scheme, enabling you to distribute your app's variations in a couple of clicks.

If you need custom configuration values (e.g. your application requires a backend URL that depends on the environment), you can make use of the configuration files we're about to create.

Creating environment configuration files

We'll create two new config files, using the Configuration Settings File type. These files will contain all environment-specific variables like backend URLs or associated domains if you're implementing universal links.

Using your configuration files

Opening your Xcode project settings, you'll see a Configurations tab.

Clicking on the + sign will allow you to duplicate the existing debug and release configurations, adding another pair for our staging environment.

Once that's done, you can assign the configuration files by selecting the respective configuration file for each configuration (you shouldn't have None on the right-hand side after that.

Your final assignment could look something like the following:

Now we just need to make sure each scheme uses the correct configuration.

Going back to Manage Schemes and selecting our staging scheme, we need to update the Build Configuration to Debug/Release Staging for every action on the left. This will make sure your builds, releases, and runs use the matching configuration values.

Adding custom environment configuration

Now, let's get to the fun part: Adding values to your configuration files. Let's go with the example we introduced earlier, and your backend requires a URL that is different for each environment or Xcode scheme in our case.

The first step is to add our new variable BACKEND_URL to the configuration files, including the respective values.

Note that URLs with protocols have to use a syntax that might look a little weird, but is required in configuration files (https:/$()/...).

Now, somehow, we need to access this from within our application. While you could use environment variables, a more straightforward way is to go to your Info.plist file and add

Once that's done, we can edit our ContentView and render the current backend URL to try out if everything works as expected:

import SwiftUI

let backendUrl = Bundle.main.infoDictionary!["BACKEND_URL"] as? String

struct ContentView: View {

    var body: some View {
        Text(backendUrl != nil ? backendUrl! : "No URL found")
            .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Note that this is an extremely simplified way to access the variable, and you should probably take measures to react accordingly if a value is missing, or process it (e.g. parsing URLs and making all variables available throughout your app).

And that's it! Switching the current Xcode scheme to staging and rebuilding the preview automatically updates to https://staging.demo.backend instead.