Tips and Tricks

Managing Build Configurations in Xcode

Resources
Stop Writing Swift That Sucks

DISCLAIMER: No Rocket Science Involved

Join 20,000+ Developers Learning About Swift Development

Download the 4 Swift Patterns I Swear By

A brand new Xcode project defines two build configurations, Debug and Release. Most projects define one or more additional build configurations for various reasons. This isn't new and it's a good practice to use build configurations to tailor a build to the specific needs of the environment it's going to be deployed to.

In this episode, I show you how to safely manage data that is specific for a build configuration, such as API keys, credentials, and other sensitive data. There are several strategies to manage build configurations, but there are important differences you need to consider, especially in the context of security.

Adding a Configuration

Fire up Xcode and create a new project by choosing the Single View App template from the iOS > Application section.

Setting Up the Project

Name the project Configurations, set Language to Swift, and make sure the checkboxes at the bottom are unchecked. Tell Xcode where you'd like to store the project and click Create.

Setting Up the Project

Open the Project Navigator on the left and click the project at the top. Select the project in the Project section to show the project details. A brand new Xcode project defines two build configurations, Debug and Release.

Build Configurations in Xcode

You commonly use the Debug configuration during development whereas the Release configuration is used for creating App Store or TestFlight builds. This probably isn't new to you.

Many applications communicate with a backend and it's a common practice to have a staging and a production environment. The staging environment is used during development. Double-click the Debug configuration and rename it to Staging.

Build Configurations in Xcode

Let's add a third configuration for the production environment. Click the + button at the bottom of the table, choose Duplicate "Staging" Configuration, and name the configuration Production.

Duplicating a Build Configuration

Duplicating a Build Configuration

Let's create a scheme for each configuration to make it easy to quickly switch between environments. Select the scheme at the top and choose Manage Schemes... from the menu. Select the scheme named Configurations and click it one more time. Rename it to Staging.

Update Schemes

With the scheme selected, click the gear icon at the bottom and choose Duplicate. Name the scheme Production. Select Run on the left and set Build Configuration to Production.

Update Schemes

That's it. We now have a build configuration for staging and production. The schemes make it quick and easy to switch between build configurations.

User-Defined Build Settings

As I mentioned earlier, there are several solutions to manage data that is specific to a particular build configuration. In this episode, I show a solution I use in any project that has some complexity to it. Before I lay out the solution I have in mind, I'd like to show you another solution that is often used by developers.

Choose the project in the Project Navigator on the left. Select the Configurations target from the Targets section and click the Build Settings tab at the top.

Target Build Settings

The Build Settings tab shows the build settings for the Configurations target. It's possible to expand this list with build settings that you define. Click the + button at the top and choose Add User-Defined Setting.

Adding a User-Defined Setting

Name the user-defined setting BASE_URL. Defining a base URL is common for applications that interact with a backend.

Adding a User-Defined Setting

What is the benefit of defining a user-defined setting? The user-defined setting allows us to set a value for BASE_URL for each build configuration. Click the triangle on the left of the user-defined setting to show the list of build configurations.

Adding a User-Defined Setting

Set the value for Production and Release to https://cocoacasts.com and the value for Staging to https://staging.cocoacasts.com.

Adding a User-Defined Setting

As the name implies, a build setting is available during the build process. Your code cannot directly access the build settings you define. That is a common misconception. There's a solution, though. Open Info.plist and add a new key/value pair. Set the key to BASE_URL and the value to $(BASE_URL).

Updating Info.plist

What is the value of adding a key/value pair to the project's Info.plist? The value of the key/value pair is updated at build time. Xcode inspects the build settings for the current build configuration and sets the value of the BASE_URL key in project's Info.plist.

Let's try it out. Open AppDelegate.swift and navigate to the application(_:didFinishLaunchingWithOptions:) method. We need to access the contents of the Info.plist file. This is possible through the infoDictionary property of the Bundle class. The bundle we are interested in is the main bundle. As the name implies, the infoDictionary property is a dictionary of key/value pairs and we access the value for the BASE_URL key.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    print(Bundle.main.infoDictionary?["BASE_URL"])
    return true
}

Select the Production scheme at the top and run the application in the simulator. Inspect the output in the console.

Optional(https://staging.cocoacasts.com)

Select the Staging scheme at the top and run the application in the simulator. Inspect the output in the console. That's pretty nice. Right?

Optional(https://staging.cocoacasts.com)

A Word About Security

Most developers find this solution very convenient. And it is. There's one problem, though. It's easy to extract the Info.plist file from applications downloaded from the App Store. What happens if you store API keys, credentials, or other sensitive information in the Info.plist file? This introduces a significant security risk that we can and should avoid.

I'd like to show you a simple solution that offers the same flexibility, type safety, and improved security. Create a new Swift file and name it Configuration.swift.

Creating a Swift File

Creating a Swift File

Define an enum with name Configuration and a raw value of type String. We define a case for each build configuration, staging, production, and release.

import Foundation

enum Configuration: String {

    // MARK: - Configurations

    case staging
    case production
    case release

}

Before we continue with the implementation of the Configuration enum, we need to update the Info.plist file. Open Info.plist and add a key/value pair. The key is Configuration and the value is $(CONFIGURATION). The value of CONFIGURATION is automatically set for us. We don't need to worry about it. As the name implies, the value is equal to the name of the build configuration with which the build is created.

Update Info.plist

Revisit Configuration.swift. We want easy access to the configuration of the build, the value stored in the project's Info.plist file. Define a static, constant property, current, of type Configuration. We access the value that is stored in the Info.plist file for the key Configuration and cast it to a String instance. We throw a fatal error if this fails because that should never happen.

import Foundation

enum Configuration: String {

    // MARK: - Configurations

    case staging
    case production
    case release

    // MARK: - Current Configuration

    static let current: Configuration = {
        guard let rawValue = Bundle.main.infoDictionary?["Configuration"] as? String else {
            fatalError("No Configuration Found")
        }
    }()

}

We use the value from the Info.plist file to create a Configuration instance. We throw another fatal error if the initialization fails. Notice that we lowercase the value stored in the Info.plist file. The Configuration instance is returned from the closure. Don't forget to append a pair of parentheses to the closure.

import Foundation

enum Configuration: String {

    // MARK: - Configurations

    case staging
    case production
    case release

    // MARK: - Current Configuration

    static let current: Configuration = {
        guard let rawValue = Bundle.main.infoDictionary?["Configuration"] as? String else {
            fatalError("No Configuration Found")
        }

        guard let configuration = Configuration(rawValue: rawValue.lowercased()) else {
            fatalError("Invalid Configuration")
        }

        return configuration
    }()

}

Let's try it out. Open AppDelegate.swift and print the current configuration. Select the Staging scheme, run the application, and inspect the output in the console.

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

Extending Configuration

You probably know that I like to leverage enums for creating namespaces. Let me show you how we can improve the current implementation of the Configuration enum. The simplest solution is to define a static, computed property, baseURL, of type URL. You can also define the property as a static, constant property. That's a personal choice. We use a switch statement to return the base URL for each build configuration.

import Foundation

enum Configuration: String {

    // MARK: - Configurations

    case staging
    case production
    case release

    // MARK: - Current Configuration

    static let current: Configuration = {
        guard let rawValue = Bundle.main.infoDictionary?["Configuration"] as? String else {
            fatalError("No Configuration Found")
        }

        guard let configuration = Configuration(rawValue: rawValue.lowercased()) else {
            fatalError("Invalid Configuration")
        }

        return configuration
    }()

    // MARK: - Base URL

    static var baseURL: URL {
        switch current {
        case .staging:
            return URL(string: "https://staging.cocoacasts.com")!
        case .production, .release:
            return URL(string: "https://cocoacasts.com")!
        }
    }

}

There are several details I'd like to point out. First, I don't use a default case. I'm explicit about the value that is returned for each build configuration. This makes it easier to spot problems and it makes the code more readable and intuitive. Second, I use the exclamation mark to force unwrap the value returned by the initializer of the URL struct. This is one of the rare scenarios in which I use the exclamation mark to force unwrap a value. It is convenient, but, more importantly, the base URL should never be equal to nil.

Open AppDelegate.swift and print the value of the baseURL computed property. With the scheme set to the Staging, run the application and inspect the output in the console.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    print(Configuration.baseURL)
    return true
}
https://staging.cocoacasts.com

This pattern has a few advantages over the first solution we implemented. We take advantage of Swift's type safety and we're no longer dealing with an optional value. We can also benefit from Xcode's autocompletion. These advantages are subtle, but you come to appreciate them over time.

Let's put the cherry on the cake by adding namespaces to the mix. Create a new Swift file and name it Configuration+DarkSky.swift.

Creating a Swift File

Creating a Swift File

Create an extension for the Configuration enum and define an enum with name DarkSky in the extension. Dark Sky is a weather service I use from time to time.

import Foundation

extension Configuration {

    enum DarkSky {

    }

}

The DarkSky enum defines a static, constant property, apiKey, of type String. We switch on the current configuration and return a different value for each build configuration. As I mentioned earlier, you can also declare the property as a static, variable property. That is up to you to decide.

import Foundation

extension Configuration {

    enum DarkSky {

        static let apiKey: String = {
            switch Configuration.current {
            case .staging:
                return "123"
            case .production:
                return "456"
            case .release:
                return "789"
            }
        }()

    }

}

There are several advantages to this approach. The configuration for the Dark Sky API is nicely namespaced. This also makes it easy to put the configuration for the Dark Sky API in a separate file.

Open AppDelegate.swift and print the API key for the Dark Sky weather service. With the scheme set to Staging, run the application and inspect the output in the console.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    print(Configuration.DarkSky.apiKey)
    return true
}
123

What's Next?

This pattern adds convenience, type safety, and security to your projects. It's easy to adopt and, once it's implemented, it's straightforward to extend. It's a pattern I enjoy using for a range of reasons. I strongly recommend to give it a try.

Resources
Stop Writing Swift That Sucks

DISCLAIMER: No Rocket Science Involved

Join 20,000+ Developers Learning About Swift Development

Download the 4 Swift Patterns I Swear By
Next Episode "Using a Branching Strategy That Works"