Translation: Yu Jin; Proofreading: Numbbbbb, WAMaker; Finalized: Pancf

Software development best practices dictate a strict separation of configuration from code. However, developers on Apple’s platforms often struggle to combine these guidelines with Xcode’s heavy project workflow.

Understanding the functionality of each project setup and how they interact is a skill that takes years to hone. But Xcode buried most of this information deep in its graphical interface, which didn’t do us any good.

Navigate to the “Build Settings” TAB in the project editor and you’ll see hundreds of Build Settings spread across project, Target, and Configuration — not to mention the other six tabs!

Fortunately, there’s a better way to manage all of your configuration without having to click through a maze of tabs and arrows.

This week, we’ll show you how to make your projects more compact, understandable, and powerful by modifying the text-based XCconfig file outside of Xcode.


The Xcode Build configuration file, also known as the xcConfig file, allows you to declare and manage the Build Setting of your APP without using Xcode. They are plain text, which means they are more friendly to code management systems and can be modified by any editor.

Basically, each configuration file consists of a series of key-value pairs with the following syntax:

<#BUILD_SETTING_NAME#> = <#value#>
Copy the code

For example, you can specify the Swift language version of your project using the SWIFT_VERSION Build Setting like this:

SWIFT_VERSION = 5.0
Copy the code

According to POSIX standards, environment variable names consist of all uppercase letters, numbers, and underscores (_) — a classic example is SCREAMING_SNAKE_CASE 🐍🗯.


At first glance, the xcconfig file bears a striking resemblance to the.env file in that both have a simple syntax, separated by newlines. However, there is more to the Xcode Build profile than meets the eye. Behold!

Keep existing values

To append new content, rather than replace existing definitions, the $(Inherited) variable can be used like this:

<#BUILD_SETTING_NAME#> = $(inherited)<#additional value#>
Copy the code

This is usually done to build a list of values, such as the search path (FRAMEWORK_SEARCH_PATHS) for the framework header file of the compiler:

FRAMEWORK_SEARCH_PATHS = $(inherited) $(PROJECT_DIR)
Copy the code

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

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

Spaces are used to separate strings from items in the path list. When specifying items that contain Spaces, you must enclose them in quotes (“).

Reference other values

You can refer to other Settings by their names using the following syntax:

<#BUILD_SETTING_NAME#> = $(<#ANOTHER_BUILD_SETTING_NAME#>)
Copy the code

This reference can be used either to define new variables from existing values or to dynamically build new values inline.

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

conditions

You can condition Build Settings by SDK (SDK), architecture (ARCH), and/or config (config) using 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#>
Copy the code

If you need to choose between multiple definitions of the same Build Setting, the compiler will resolve it based on the constraints.

<#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#>
Copy the code

For example, you can speed up native builds by using the Build Setting below to specify that only active architecture is compiled.

ONLY_ACTIVE_ARCH[config=Debug][sdk=*][arch=*] = YES
Copy the code

Reference Settings from other configuration files

Like the #include directive in C, Build profiles can use this syntax to reference Settings in other profiles.

#include "<#path/to/File.xcconfig#>"
Copy the code

As we’ll see later in this article, you can take advantage of this to Build cascading lists of Build Settings in a very powerful way.

Normally, the compiler will report an error when encountering a #include directive that cannot be parsed. But the xcconfig file also supports #include? Directive, under which the compiler does not report an error if the file cannot be found.

There are not many cases where compile-time behavior is changed based on the presence or absence of a file; After all, builds should be predictable. But you can use it with an optional development tool, such as Reveal, which requires the following configuration:

> # Reveal.xcconfig
Copy the code

OTHER_LDFLAGS = (inherited) /Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries

Create a Build profile

To create a Build profile, select “File > New File…” Menu item (⌘N), drop down to the “Other” section and select the Configuration Settings File template. Save it to your project directory and make sure it is at the target you expect.

Once the XCConfig file is created, you can assign it to one or more Build configurations for the target.


Now that we’ve covered the basics of using Xcode Build profiles, let’s look at a few examples of how they can be used to manage development, stage, and Production environments.


Provide custom APP names and ICONS for build versions

When developing iOS applications, it is common to have various build versions installed on emulators and test devices (as well as the latest version of the app Store for your reference).

Using the xcConfig file, you can easily assign a different name and APP icon to each configuration.

Xcconfig PRODUCT_NAME = $(inherited) α ASSETCATALOG_COMPILER_APPICON_NAME = appicon-alpha -- // Staging. Xcconfig PRODUCT_NAME = $(Inherited) β ASSETCATALOG_COMPILER_APPICON_NAME = appicon-betaCopy the code

Manage constants in different environments

If your back-end developers also follow the 12-Factor App theory mentioned earlier, they will have separate interfaces for development, stage, and Production environments.

Probably the most common way to manage environments on iOS is to use Build Setting such as conditional compilation statements + 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
Copy the code

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

Another option is to put these environment-specific values where they belong — in the xcConfig file.

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

---

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

Unfortunately, xcconfig treats all // as comment separators, whether or not they are enclosed in quotes. If you escape with backslashes \/\/, these backslashes will also be displayed directly and must be removed from the result when used. This is especially inconvenient when specifying URL constants for each environment.

If you don’t want to deal with this hassle, you can ignore Scheme in xcConfig and add https:// to your code. (You are using HTTPS… Right?)

To get these values programmatically, however, we need an additional step:

Access Build Setting in Swift

Build Setting defined by the Xcode project file, xcConfig file, and environment variables is only available at Build time. When you run a compiled APP, all relevant context is invisible. (Thank Goodness!)

But wait — don’t you remember seeing some Build Setting in other tabs before? Info, right?

In fact, the Info TAB is just a sock in target’s info.plist file. During Build, the info.plist file is compiled according to the Build Setting configuration and copied into the final APP bundle. Therefore, after adding references to $(API_BASE_URL) and $(API_KEY), you can access these values through the infoDictionary property of the Foundation Bundle API. Perfect!

In this way, we can do 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") } }Copy the code

From a call point of view, we found that this approach worked perfectly with our best practices — there wasn’t a hard-coded constant!

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

Don’t write anything private in code. Instead, store them securely in a password manager or something similar.

To prevent your privacy from being leaked to GitHub, add the following configuration to your.gitignore file (as needed) :

> # .gitignore
Copy the code

Development.xcconfig Staging.xcconfig Production.xcconfig

Some developers prefer to use contains the required key placeholder file (such as Development. Sample. Xcconfig) instead of the documents. When the code is pulled, the developer copies the file to a non-placeholder location and populates it accordingly.



Xcode projects are large, fragile, and opaque. They are a source of friction when team members work together and are often a drag on their work.

Fortunately, the XCConfig file addresses these pain points nicely. Moving your configuration from Xcode to xcConfig files has the benefit of keeping your project at arm’s length from the details of Xcode and not being held back by Apple.

This article is translated by SwiftGG translation team and has been authorized to be translated by the authors. Please visit swift.gg for the latest articles.