Building Your App Using Build Configurations and .xcconfig
Use Xcode build settings and .xcconfig files to change your app’s settings and icon with different build configurations. By Saleh Albuga.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Building Your App Using Build Configurations and .xcconfig
30 mins
- Getting Started
- Setting Up Widget with App Group
- Demystifying Build Settings and Build Configurations in Xcode
- Understanding Targets and Schemes
- Creating a Staging Environment Configuration
- Creating Configuration Settings Files
- Working With Configuration Settings Files
- Creating a User-Defined Setting to Change the Bundle Display Name
- Retaining Values With Inheritance
- Referencing Values of Other Settings
- Accessing Settings From Code
- Adding Conditions
- Creating Record Stores for Each Environment
- Using Configuration Files for Each Target
- Differentiating the App Icon for Non-Release Builds
- Where to Go From Here?
Debug, test, release — these are the phases most apps go through. In each phase, the app has different build settings, definitions and constants. Developers build an app with debug back-end URLs and settings. Testers test beta builds with production-like settings. Customers use the app with the final production settings.
Managing these settings across different environments in Xcode is time-consuming — not to mention the added work when you have multiple targets. Fortunately, Apple has provided a much better way to work with these settings: Xcode build configuration files, or .xcconfig files.
In this tutorial, you’ll:
- Work with Xcode build configuration files.
- Manage build settings across multiple environments and targets.
- Access build settings from code.
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Open the project. Build and run.
The app you’ll work on, NinjaCounter, helps biologists and enthusiasts count turtle hatchlings. The app has one view: CounterView.swift, where users record the hatchlings.
In the NinjaCounter group, you’ll find the following:
- Hatchling: A simple struct that has the hatchling record attributes.
-
UserDefaultsHelper: A
UserDefaults
helper that provides methods to store and load hatchling records.
Build and run. In the tag text field, enter Leonardo. Tap the + Hatchling button.
With that, you created a new record with the hatchling tag and hatch time.
Now that you get the gist of the starter project, you’ll set up the app widget.
Setting Up Widget with App Group
Open Widget.swift and take a look at the code. It creates a simple widget that shows the number of hatchlings counted and the tag of the last hatchling reported.
In getTimeline(in:completion:)
, the widget uses UserDefaultsHelper
‘s getRecordsCount()
and getRecords()
to get the data from UserDefaults
.
Select the WidgetExtension scheme. Build and run.
Currently, the widget doesn’t show any data, even though you just recorded Leonardo. That’s because the extensions don’t have access to the app’s UserDefaults
. To solve this, you’ll add the app and widget to an app group.
Select the NinjaCounter project in the Project navigator to show the Project Editor. Select NinjaCounter target. Open the Signing & Capabilities tab.
Select a development team for signing.
Change the Bundle Identifier to something unique to you such as com.myorg.NinjaCounter. Remember this as you’ll need again momentarily.
Click + Capability. Double-click App Groups.
Now that you’ve added App Groups to the capabilities, click the + button to create an app group. You’ll see a prompt for the group name.
Enter group.
Now, perform the same steps to the Widget Extension target. Be sure the bundle identifier ends with .widget, but use the same app group that you created for the main app. This allows data sharing between the host app and the widget extension.
Now that you’ve created your App Group and added your targets to it, it’s time to let the app group access the UserDefaults
suite.
Open UserDefaultsHelper.swift and replace the declaration of defaults
with:
static private let defaults = UserDefaults(
suiteName: "<#the app group name you defined#>")
?? .standard
With this code, you ensure the app saves and reads data from the UserDefaults
suite that the app group shares. This lets the widget access the hatchling data.
Change the active scheme to NinjaCounter. Build and run.
The record you added isn’t there anymore because you’re using a different UserDefaults
suite. Add Leonardo again!
Change the active scheme to WidgetExtention. Build and run.
You can see the widget shows the added record now. Congratulations!
In the next section, you’ll explore build settings in Xcode.
Demystifying Build Settings and Build Configurations in Xcode
In this section, you’ll see how Xcode displays and resolves build settings. Open the Project Editor. Locate the TARGETS list. Select NinjaCounter as the app target.
Select the Build Settings tab. Select the All and Levels build settings filter options.
Here, you see the build settings of the app target. The build settings are separated into four columns displaying the settings values in different scopes.
- Resolved: The actual values after resolving precedence.
- NinjaCounter (target): Displays the values set at the target level. Target build settings have a higher precedence than the project’s. By default, targets inherit values from the project build settings.
- Ninja Counter (project): Shows the values set in the project’s build settings. General build settings are available at the project level, others are only available for targets.
- iOS Default: Shows the iOS default value of a setting.
- Platform defaults
- Project.xcconfig file
- Project file build settings
- Target .xcconfig file
- Target build settings
Select the WidgetExtension target. Look at the build settings.
You can see the same settings but at the widget’s target level.
Settings have multiple values, one for each Build Configuration. Check Base SDK, for example. A build configuration is like an environment.
You define Build Configurations globally, at the project level. Xcode creates two configurations for you: Debug and Release.
The default values for the build settings are different for these environments. For example, Clang Optimization Level is set to None -O0
in Debug, letting you debug and inspect the code. Meanwhile, in Release, it defaults to Fastest, Smallest -Os
for maximum code optimization and the smallest executable size.
Now that you’ve covered the build settings, it’s time to go over targets and schemes.
Understanding Targets and Schemes
A target specifies a single product with its build settings and files. A target can be an app, extension, framework, iMessage app, App Clip and so on.
When the widget was created, Xcode created a new target. You configured the development team and capabilities for both the app and the widget targets.
Apple defines an Xcode scheme as follows: “An Xcode scheme defines a collection of targets to build, a configuration to use when building, and a collection of tests to execute.”
Creating a new target automatically creates a new scheme that goes with it.
To see this, click the active scheme. Click Edit Scheme….
You’ll see the scheme editor:
The WidgetExtention scheme, for instance, defines how Xcode will build, run, test, profile, analyze and archive the widget target. It also defines which build configurations to use with these actions.
You can see that Run defaults to the Debug configuration, while Archive defaults to Release. The Build Configuration drop-down menu is where you change the selected build configuration.
Close the scheme editor. Next, you’ll create a new build configuration for the staging environment.
Creating a Staging Environment Configuration
To configure and create test builds of the app, you need a staging build configuration.
Open the Project Editor and select the NinjaCounter PROJECT. Open the Info tab.
This is where you define build configurations. They’re global and shared among targets. Click + to add a new one.
Select Duplicate “Debug” Configuration. Name the new configuration Staging.
You’ll see the new Staging configuration. Select the NinjaCounter target and switch to the Build Settings tab.
The settings now have new values for the new build configuration. You duplicated the build configuration from Debug, so they have the same values.
Now that you’ve created your staging environment, it’s time to add .xcconfig files.
Creating Configuration Settings Files
Using the Build Settings tab to modify settings has some disadvantages. Searching through long lists with different scopes is time-consuming, especially when you have multiple targets, build configurations and settings to manage. .xcconfig files are an alternative that simplifies this process.
Select the NinjaCounter group in Project navigator. Create a new group. Name the group Config Files. This is where you’ll place your configuration files.
Click File ▸ New ▸ File….
Select Configuration Settings File in the file templates list.
Name the file Debug.xcconfig. For this app, you need to create an .xcconfig file for each configuration. Create Staging.xcconfig and Release.xcconfig.
Next, you’ll set your configuration files in Xcode.
Working With Configuration Settings Files
To use configuration files you need to set them in Xcode. Before that, add a setting for the app display name.
Open Debug.xcconfig. Add the setting below and save.
APP_NAME = Ninja Counter
Settings in .xcconfig files use the following syntax: SETTING_NAME = VALUE.
Ninja Counter
does. The exception is when the string that contains the space is in a string list, which is a space-separated list of string values.
Open the Project Editor and select the NinjaCounter project. Click the Info tab.
The Configurations section is where you set configuration files.
As you can see, you can use configuration files at the project level as well as at each target level, for each environment.
Under Based on Configuration File, click on a configuration file option.
Xcode shows you the list of .xcconfig files you’ve created. Set the corresponding configuration file for each build configuration to the app and widget targets, as shown below:
- Debug
- NinjaCounter: Debug
- WidgetExtension: Debug
- Staging
- NinjaCounter: Staging
- WidgetExtension: Staging
- Release
- NinjaCounter: Release
- WidgetExtension: Release
At this point, you’ve created a configuration file for each environment. Good job! Next, you’ll customize the settings for each environment.
Creating a User-Defined Setting to Change the Bundle Display Name
As a developer, you use and work with your app in all its phases. When you switch between different builds, it can be confusing to know which build you currently have installed. For that reason, having different display names is useful.
Open Debug.xcconfig. Change APP_NAME
‘s value to Ninja CounterDev. This is the app display name you’ll see when you install a debug build.
Now, you’ll know at a glance that this is the development build that you’re working on with the top-secret and next-gen features! :]
Next, you need to change the names for the other configurations. Open Staging.xcconfig and add the following line:
APP_NAME = Ninja CounterQA
Finally, open Release.xcconfig and update the setting with the production display name:
APP_NAME = Ninja Counter
Next, select the project itself in the Project navigator. Select the NinjaCounter app target.
Open the Build Settings tab. Scroll to the end of list.
Now, you can see your user-defined build setting APP_NAME
and a new scope of settings that shows values at the configuration files level. If you see the values you set in the configuration file here, you’re on the right track!
The last step to changing the app display name is to set Bundle display name to refer to your user-defined setting.
Open NinjaCounter’s Info.plist. Add the Bundle display name property. Set the value of the property to $(APP_NAME).
Optionally, you can also add it from the Info tab from the Project Editor.
Make sure you set NinjaCounter as the active scheme.
Build and run. Press Shift-Command-H to enter to the home screen.
Now, you can see your development build app name.
It’s time to test the app in the Staging environment. Option-click the the run button in the Xcode task bar.
Next, select the Run action, then the Info tab and click the Build Configuration drop-down menu. Your new staging configuration is in the list. Select Staging as the building configuration.
Click Run, wait for the app to start, then return the iOS home screen.
The app display name is now NinjaCounterQA, showing you immediately which build you are using.
Next, you’ll create a base configuration file to avoid redundant values.
Retaining Values With Inheritance
In your configuration file, you set the APP_NAME
for your different builds to Ninja CounterDev
, Ninja CounterQA
and Ninja Counter
. The release build name, NinjaCounter
, is the base value, which you repeat in each name.
It’s a best practice to set general build settings at the project level. Then, reuse those settings in different configurations to avoid redundancy. In this case, you’ll change APP_NAME
in each build configuration to inherit from the base value.
Right-click the Config files group. Select New file…. Create a new configuration file named Base.xcconfig and add the following line:
APP_NAME = Ninja Counter
Next, replace the content of the other configuration files as follows below.
Debug.xcconfig:
#include "Base.xcconfig" APP_NAME = $(inherited)Dev
Staging.xcconfig:
#include "Base.xcconfig" APP_NAME = $(inherited)QA
Release.xcconfig:
#include "Base.xcconfig"
Here, you changed the three configuration files to inherit and reuse the base settings from Base.xcconfig. You did this by using $(inherited)
and appending the part of the name specific to each build.
$(inherited)
values are referred from the included files, if any, and follow the precedence mentioned earlier. Similarly , if you inherit a system build setting in a configuration file, the resolved value will be set based on the precedence.
Finally, build and run the app in Staging.
The app name displays correctly. Note that you didn’t need to add the APP_NAME
definition to Release.xcconfig. That’s because the settings fall back to the inherited values.
To see how the settings display in Xcode, open the Project Editor. Select the NinjaCounter target, then Build Settings. Scroll to the User-defined section.
The resolved values don’t look correct despite displaying correctly when the app runs. That’s because Base.xcconfig isn’t set in the Configurations part of the Project Editor. However, the inheritance resolves as expected in runtime.
To fix this, select the project in the project editor. Under Configurations, set Base.xcconfig at the project level of all three configurations.
Now, go back to the Build Settings of the app target.
The values display correctly now in Xcode. Note the new scope shows the project-level configuration file.
Next, you’ll add more custom settings.
Referencing Values of Other Settings
Now that your environments are ready, it’s time to add more settings. Add the following to Base.xcconfig. Be sure to substitute your bundle identifier:
BASE_BUNDLE_IDENTIFIER = <your bundle identifier> PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER)
BASE_BUNDLE_IDENTIFIER
is a user-defined setting that contains the project’s base bundle identifier, while PRODUCT_BUNDLE_IDENTIFIER
is an Xcode build setting that specifies the bundle identifier.
Here, you’ve referenced BASE_BUNDLE_IDENTIFIER
‘s value. The reference syntax is: $(OTHER_BUILD_SETTING_NAME).
Now, if you were to change the value of BASE_BUNDLE_IDENTIFIER in Base.xcconfig, you wouldn’t see the change reflected in the build settings. That’s because the bundle identifier is currently hard-coded in the target’s settings. To fix this, return to the Project Editor and select the NinjaCounter target. In the search field, type bundle to narrow down the number of settings show. Double-click the target build setting and change it’s value to:
$(inherited)
Previously, you changed the code in UserDefaultsHelper.swift to use the app group identifier as the UserDefaults
suite name, as below.
UserDefaults(suiteName: "group.<your bundle identifier>")
To avoid hard coding values like the suite name, add the following setting below PRODUCT_BUNDLE_IDENTIFIER
in Base.xcconfig:
USER_DEFAULTS_SUITE_NAME = group.$(BASE_BUNDLE_IDENTIFIER)
Since the app group identifier in NinjaCounter depends on the bundle identifier, you created USER_DEFAULTS_SUITE_NAME
. It references BASE_BUNDLE_IDENTIFIER
inline and appends the “group.” prefix. In a moment, you’ll update your code to use this new setting but first you must update the app to use the setting. In the Project navigator, you’ll find two .entitlements files. Open WidgetExtension.entitlements and click the disclosure arrows to open the settings. Change the value of Item 0 to:
$(USER_DEFAULTS_SUITE_NAME)
Next, open NinjaCounter.entitlements and do the same thing.
You ensured consistency of build settings that depend on other settings by referencing their values. Next, you’ll use that setting in your code and remove the hard-coded one.
Accessing Settings From Code
To access a build setting from code, you first need to add a reference property in Info.plist. Open NinjaCounter’s Info.plist and add a custom property named:
USER_DEFAULTS_SUITE_NAME
With the value:
$(USER_DEFAULTS_SUITE_NAME)
Do the same to the widget’s Info.plist because the widget needs to access the settings as well.
Next, create a new Swift file in the NinjaCounter group and name it Config.swift. Open the file and add the following code:
enum Config {
static func stringValue(forKey key: String) -> String {
guard let value = Bundle.main.object(forInfoDictionaryKey: key) as? String
else {
fatalError("Invalid value or undefined key")
}
return value
}
}
stringValue(forKey:)
is a helper method that simplifies retrieving values from Info.plist. It calls Bundle
‘s object(forInfoDictionaryKey:)
to obtain a string value.
Then, in the File inspector, select WidgetExtension as a target in addition to NinjaCounter.
Open UserDefaultsHelper.swift and again replace the declaration of defaults
with:
static private let defaults = UserDefaults(
suiteName: Config.stringValue(forKey: "USER_DEFAULTS_SUITE_NAME"))
?? .standard
Here, you changed UserDefaultsHelper
‘s code to get the suite name from the build setting. You used Config
‘s method, stringValue(forKey:)
, to fetch the value.
Next, you’ll add conditional settings.
Adding Conditions
You can add conditions to build settings to target a specific SDK, architecture or build configuration. This is especially useful when adding project-wide settings that aren’t specific to a target.
In Base.xcconfig, add the following line:
ONLY_ACTIVE_ARCH[config=Debug][sdk=*][arch=*] = YES
Here, you set ONLY_ACTIVE_ARCH
to YES when the app builds in Debug configuration. As a result, it speeds up build times by building only the active architecture.
Conditional settings follows the following syntax:
BUILD_SETTING_NAME[condition=value] = VALUE.
Setting the conditional value to an asterisk means any value. The setting you added above applies for any sdk and any architecture, but only for the Debug configuration.
In the next section, you’ll customize the app’s behaviors across the different environments.
Creating Record Stores for Each Environment
Now that your environments are ready, you’ll store the hatchlings’ records in different UserDefaults
keys — one per environment. Since the app stores data locally, UserDefaults
here is the app’s “back end”.
Time to add the UserDefaults
key in your environment’s configuration files.
In Debug.xcconfig, add:
USER_DEFAULTS_RECORDS_KEY = HatchlingsRecords-Debug
In Staging.xcconfig, add:
USER_DEFAULTS_RECORDS_KEY = HatchlingsRecords-Staging
In Release.xcconfig, add:
USER_DEFAULTS_RECORDS_KEY = HatchlingsRecords
The key name changes in each environment.
Next, add the following property below to the Info.plist of NinjaCounter and extension.
Key:
USER_DEFAULTS_RECORDS_KEY
Value:
$(USER_DEFAULTS_RECORDS_KEY)
After that, open UserDefaultsHelper.swift and replace the declaration of recordsKey
with:
static private let recordsKey = Config
.stringValue(forKey: "USER_DEFAULTS_RECORDS_KEY")
Just as you obtained the UserDefaults
suite name from the build settings, here, in the changes above, you replaced the hard-coded UserDefaults
key with the corresponding build settings values using stringValue(forKey:)
.
Finally, you’re ready to test your changes! Change the active scheme’s Build Configuration to Debug.
Build and run. Add a Donatello record.
Next, change the Build Configuration to Release. Build and run.
You don’t see Donatello‘s record because you stored it in the Debug store. Change the Build Configuration back to Debug. Build and run.
The record you stored while running with the debug build configuration is there. Now, you know your environments work as intended.
Does this remind you of a different approach? Look at the code below.
#if DEBUG
let recordsKey = "HatchlingsRecords-Debug"
#elseif STAGING
let recordsKey = "HatchlingsRecords-Staging"
#else
let recordsKey = "HatchlingsRecords"
#endif
Conditional compilation is a popular practice. However, using it to manage constants across environments has a downside where you mix configurable constants with the code. Using build settings to store these values and use them in code is a good alternative.
That was easy! Having settings in configuration files not only simplifies managing build settings, but also lets you externalize settings from Xcode. This makes them portable and makes tracking changes in source control more convenient.
In the next section, you’ll create configuration files for each target.
Using Configuration Files for Each Target
The app is now fully functional in all three environments. But what if you need to create or externalize a target-specific build setting for the WidgetExtention? Next, you’ll see how to create configuration files for the widget target.
In the Config files group, create the following configuration files:
-
WidgetBase.xcconfig:
#include "Base.xcconfig" PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER).widget
-
WidgetDebug.xcconfig:
#include "Debug.xcconfig" #include "WidgetBase.xcconfig"
-
WidgetStaging.xcconfig:
#include "Staging.xcconfig" #include "WidgetBase.xcconfig"
-
WidgetRelease.xcconfig:
#include "Release.xcconfig" #include "WidgetBase.xcconfig"
Here, you set the widget PRODUCT_BUNDLE_IDENTIFIER
by appending BASE_BUNDLE_IDENTIFIER
to your base setting.
Next, set the files to their respective environments for the WidgetExtension target in the Project Editor as below.
- Debug
- Project: Base
- NinjaCounter: Debug
- WidgetExtension: WidgetDebug
- Staging
- Project: Base
- NinjaCounter: Staging
- WidgetExtension: WidgetStaging
- Release
- Project: Base
- NinjaCounter: Release
- WidgetExtension: WidgetRelease
Finally, return to the Project Editor and change the Product Bundle Extension setting for the WidgetExtension target to:
$(inherited)
After that, change the active scheme to WidgetExtension. Ensure the build configuration is Debug. Build and run.
The widget now shows the latest record you added for Donatello. Even though WidgetDebug.xcconfig doesn’t contain any settings, it includes the settings from WidgetBase.xcconfig and Debug.xcconfig. That’s why it worked when you set Debug.xcconfig for the widget.
Differentiating the App Icon for Non-Release Builds
Some stakeholders, like testers and QA engineers, use multiple builds of an app. For example, they might use the App Store build and a Staging build provided by developers or TestFlight. To make the lives of your colleagues easier, your next step will be to change the app icon to distinguish Debug and Staging builds from Release builds.
In the project, you’ll find another app icon set called AppIcon-NonProd in the app’s Assets.xcassets.
To set the name of the asset catalog app icon set, you need to modify ASSETCATALOG_COMPILER_APPICON_NAME
in the configuration files.
In Debug.xcconfig and Staging.xcconfig, add:
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-NonProd
In Release.xcconfig, add:
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
Set the active scheme to the NinjaCounter. Set the Build Configuration to Debug. Build and run. Enter the home screen.
The icon didn’t change because the Target Build Settings have higher precedence. To fix this, go to the Build Settings of NinjaCounter’s target from the project editor and locate Asset Catalog App Icon Set Name.
In the configuration file’s scope, you can see the values you added to the configuration files. However, the setting resolves to the value at the target scope.
To fix this, change the parent value at the app target scope to $(inherited).
The resolved values match the configuration files now.
Build and run. Enter the home screen.
The app now shows the icons from the AppIcon-NonProd app icon set.
Where to Go From Here?
You can download the final project using the Download Materials button at the top or bottom of this tutorial.
In this tutorial, you used .xcconfig files to externalize build settings. You covered:
- Managing settings across different build configurations.
- Multiple approaches to using configuration files for different environments and targets.
- Accessing build settings from code.
For more about build configuration and app distribution best practices, check out our iOS App Distribution & Best Practices book.
Here are some more helpful resources from Apple’s documentation:
I hope you’ve enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!