Xcode Build Configuration Files

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

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

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.

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

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!

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")


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 a Cupertino-approved happy path.

NSMutableHipster

Questions? Corrections? Issues and pull requests are always welcome.

This article uses Swift version 5.0. Find status information for all articles on the status page.

Next Article

Poor NSCache, always being overshadowed by NSMutableDictionary. It’s as if no one knew how it provides all of that garbage collection behavior that developers take great pains to re-implement themselves.