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.

Leave a rating/review
Download materials
Save for later
Share

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.

NinjaCounter app

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.

New hatchling recorded added

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.

App widget showing 0 hatchlings

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.

Adding App Group capability

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.. Click OK.

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.

App showing main page, no data

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.

App widget showing one record

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.

NinjaCounter app's build settings

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
Note: Build settings follow the precedence below, from lowest to highest:
  • Platform defaults
  • Project.xcconfig file
  • Project file build settings
  • Target .xcconfig file
  • Target build settings

Select the WidgetExtension target. Look at the build settings.

Widget 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….

Select Edit scheme...

Pro tip: Go directly to the scheme editor without passing the menu by Option-clicking the active scheme.

You’ll see the scheme editor:

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.