Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
@mattt mattt Copy editing 73361f0 May 22, 2019
1 contributor

Users who have contributed to this file

436 lines (338 sloc) 12.5 KB
title author category excerpt status
Xcode Build Configuration Files
Mattt
Xcode
Software development best practices prescribe strict separation of configuration from code. Learn how you can use `xcconfig` files to make your Xcode projects more compact, comprehensible, and powerful.
swift
5.0

Software development best practices prescribe strict separation of configuration from code. Yet developers on Apple platforms often struggle to square these guidelines with Xcode's project-heavy workflow.

Understanding what each project setting does and how they all interact with one another is a skill that can take years to hone. And the fact that much of this information is buried deep within the GUIs of Xcode does us no favors.

Navigate to the "Build Settings" tab of the project editor, and you'll be greeted by hundreds of build settings spread across layers of projects, targets, and configurations --- and that's to say nothing of the other six tabs!

Xcode build settings

Fortunately, there's a better way to manage all of this configuration that doesn't involve clicking through a maze of tabs and disclosure arrows.

This week, we'll show you how you can use text-based xcconfig files to externalize build settings from Xcode to make your projects more compact, comprehensible, and powerful.


Xcode build configuration files, more commonly known by their xcconfig file extension, allow build settings for your app to be declared and managed without Xcode. They're plain text, which means they're much friendlier to source control systems and can be modified with any editor.

Fundamentally, each configuration file consists of a sequence of key-value assignments with the following syntax:

<#BUILD_SETTING_NAME#> = <#value#>

For example, to specify the Swift language version for a project, you'd specify the SWIFT_VERSION build setting like so:

SWIFT_VERSION = 5.0

{% info %}

According to the POSIX standard environment variables have names consisting solely of uppercase letters, digits, and underscore (_) --- a convention I like to call SCREAMING_SNAKE_CASE 🐍🗯.

{% endinfo %}


At first glance, xcconfig files bear a striking resemblance to .env files, with their simple, newline-delimited syntax. But there's more to Xcode build configuration files than meets the eye. Behold!

Retaining Existing Values

To append rather than replace existing definitions, use the $(inherited) variable like so:

<#BUILD_SETTING_NAME#> = $(inherited)<#additional value#>

You typically do this to build up lists of values, such as the paths in which the compiler searches for frameworks to find included header files (FRAMEWORK_SEARCH_PATHS):

FRAMEWORK_SEARCH_PATHS = $(inherited) $(PROJECT_DIR)

Xcode assigns inherited values in the following order (from lowest to highest precedence):

  • Platform Defaults
  • Xcode Project File Build Settings
  • xcconfig File for the Xcode Project
  • Active Target Build Settings
  • xcconfig File for the Active Target

{% info %} Spaces are used to delimit items in string and path lists. To specify an item containing whitespace, you must enclose it with quotation marks ("). {% endinfo %}

Referencing Values

You can substitute values from other settings by their declaration name with the following syntax:

<#BUILD_SETTING_NAME#> = $(<#ANOTHER_BUILD_SETTING_NAME#>)

Substitutions can be used to define new variables according to existing values, or inline to build up new values dynamically.

OBJROOT = $(SYMROOT)
CONFIGURATION_BUILD_DIR = $(BUILD_DIR)/$(CONFIGURATION)-$(PLATFORM_NAME)

Conditionalizing

You can conditionalize build settings according to their SDK (sdk), architecture (arch), and / or configuration (config) according to the following syntax:

<#BUILD_SETTING_NAME#>[sdk=<#sdk#>] = <#value for specified sdk#>
<#BUILD_SETTING_NAME#>[arch=<#architecture#>] = <#value for specified architecture#>
<#BUILD_SETTING_NAME#>[config=<#configuration#>] = <#value for specified configuration#>

Given a choice between multiple definitions of the same build setting, the compiler resolves according to specificity.

<#BUILD_SETTING_NAME#>[sdk=<#sdk#>][arch=<#architecture#>] = <#value for specified sdk and architectures#>
<#BUILD_SETTING_NAME#>[sdk=*][arch=<#architecture#>] = <#value for all other sdks with specified architecture#>

For example, you might specify the following build setting to speed up local builds by only compiling for the active architecture:

ONLY_ACTIVE_ARCH[config=Debug][sdk=*][arch=*] = YES

Including Settings from Other Configuration Files

A build configuration file can include settings from other configuration files using the same #include syntax as the equivalent C directive on which this functionality is based:

#include "<#path/to/File.xcconfig#>"

As we'll see later on in the article, you can take advantage of this to build up cascading lists of build settings in really powerful ways.

{% info %} Normally when the compiler encounters an #include directive that can't be resolved, it raises an error. But xcconfig files also support an #include? directive, that doesn't complain if the file can't be found.

There aren't many cases in which you'd want the existence or nonexistence of a file to change compile-time behavior; after all, builds are best when they're predictable. But you might use this as a hook for optional development tools like Reveal, which requires the following configuration:

# Reveal.xcconfig
OTHER_LDFLAGS = $(inherited) -weak_framework RevealServer
FRAMEWORK_SEARCH_PATHS = $(inherited) /Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries

{% endinfo %}

Creating Build Configuration Files

To create a build configuration file, select the "File > New File..." menu item (N), scroll down to the section labeled "Other", and select the Configuration Settings File template. Next, save it somewhere in your project directory, making sure to add it to your desired targets

Xcode new configuration file

Once you've created an xcconfig file, you can assign it to one or more build configurations for its associated targets.

Xcode project configuration


Now that we've covered the basics of using Xcode build configuration files let's look at a couple of examples of how you can use them to manage development, stage, and production environments.


Customizing App Name and Icon for Internal Builds

Developing an iOS app usually involves juggling various internal builds on your simulators and test devices (as well as the latest version from the App Store, to use as a reference).

You can make things easier on yourself with xcconfig files that assign each configuration a distinct name and app icon.

// Development.xcconfig
PRODUCT_NAME = $(inherited) α
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Alpha

---

// Staging.xcconfig
PRODUCT_NAME = $(inherited) β
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Beta

Managing Constants Across Different Environments

If your backend developers comport themselves according to the aforementioned 12 Factor App philosophy, then they'll have separate endpoints for development, stage, and production environments.

On iOS, perhaps the most common approach to managing these environments is to use conditional compilation statements with build settings like DEBUG.

import Foundation

#if DEBUG
let apiBaseURL = URL(string: "https://api.example.dev")!
let apiKey = "9e053b0285394378cf3259ed86cd7504"
#else
let apiBaseURL = URL(string: "https://api.example.com")!
let apiKey = "4571047960318d233d64028363dfa771"
#endif

This gets the job done, but runs afoul of the canon of code / configuration separation.

An alternative approach takes these environment-specific values and puts them where they belong --- into xcconfig files.

// Development.xcconfig
API_BASE_URL = api.example.dev
API_KEY = 9e053b0285394378cf3259ed86cd7504

---

// Production.xcconfig
API_BASE_URL = api.example.com
API_KEY = 4571047960318d233d64028363dfa771

{% error %}

Unfortunately, xcconfig files treat the sequence // as a comment delimiter, regardless of whether it's enclosed in quotation marks. If you try to escape with backslashes \/\/, those backslashes show up literally and must be removed from the resulting value. This is especially inconvenient when specifying per-environment URL constants.

If you'd rather not work around this unfortunate behavior, you can always omit the scheme and prepend https:// in code. (You are using https... right?)

{% enderror %}

However, to pull these values programmatically, we'll need to take one additional step:

Accessing Build Settings from Swift

Build settings defined by the Xcode project file, xcconfig files, and environment variables, are only available at build time. When you run the compiled app, none of that surrounding context is available. (And thank goodness for that!)

But wait a sec --- don't you remember seeing some of those build settings before in one of those other tabs? Info, was it?

As it so happens, that info tab is actually just a fancy presentation of the target's Info.plist file. At build time, that Info.plist file is compiled according to the build settings provided and copied into the resulting app bundle. Therefore, by adding references to $(API_BASE_URL) and $(API_KEY), you can access the values for those settings through the infoDictionary property of Foundation's Bundle API. Neat!

Xcode Info.plist

Following this approach, we might do something like the following:

import Foundation

enum Configuration {
    static func value<T>(for key: String) -> T {
        guard let value = Bundle.main.infoDictionary?[key] as? T else {
            fatalError("Invalid or missing Info.plist key: \(key)")
        }

        return value
    }
}

enum API {
    static var baseURL: URL {
        return URL(string: "https://" + Configuration.value(for: "API_BASE_URL"))!
    }

    static var key: String {
        return Configuration.value(for: "API_KEY")
    }
}

When viewed from the call site, we find that this approach harmonizes beautifully with our best practices --- not a single hard-coded constant in sight!

let url = URL(string: path, relativeTo: API.baseURL)!
var request = URLRequest(url: url)
request.httpMethod = method
request.addValue(API.key, forHTTPHeaderField: "X-API-KEY")

{% warning %}

Don't commit secrets to source code. Instead, store them securely in a password manager or the like.

To prevent secrets from leaking onto GitHub, add the following to your .gitignore file (as appropriate):

# .gitignore
Development.xcconfig
Staging.xcconfig
Production.xcconfig

In place of those files, some developers like to have placeholder files (e.g. Development.sample.xcconfig) with all of the expected keys. When checking out the repository, the developer copies this file to the non-placeholder location and fills it out accordingly.

{% endwarning %}



Xcode projects are monolithic, fragile, and opaque. They're a source of friction for collaboration among team members and generally a drag to work with.

Fortunately, xcconfig files go a long way to address these pain points. Moving configuration out of Xcode and into xcconfig files confers a multitude of benefits and offers a way to distance your project from the particulars of Xcode without leaving the Cupertino-approved "happy path".

You can’t perform that action at this time.