Leveraging xcconfig files in shared projects
Overview
The best way to try out new libraries or frameworks is often through sample projects or even small scoped apps. Recently I’ve been toying with CloudKit
in a small app, and wanted to pair with a friend who was also interested in learning more about it.
We setup a small project in a shared git repository which we were both able to build and run on our simulators. However when it got to adding the CloudKit
bit to our project and start testing on our devices, that’s when the fun began.
Both of us have our own individual developer accounts, which allowed us to individually select our team in the project settings to get the app running on our devices. However that tweaked the project settings which was in source control and seeing we’re not part of a shared developer account (e.g. a team account), those changes were incompatible with one another!
In this post, I’ll share how we solved this issue by leveraging xcconfig
configuration files.
The short version
In order to leverage automatic signing in Xcode, the PRODUCT_BUNDLE_IDENTIFIER
and DEVELOPMENT_TEAM
settings need to be set. Those will differ between developer accounts.
xcconfig
files can be used to declare these kind of settings. The optional include syntax #include?
can then be used within these files to allow referencing other xcconfig
files that can host local overrides for each developer which can be excluded from source control.
In addition, we can specify our own custom settings like MY_CLOUDKIT_CONTAINER_ID
which can then be used within the entitlements file of the app.
Read on for the details …
Setup
1) Create a new config file
Config files have the extension .xcconfig
, they can be created manually or via Xcode.
For this example, let’s name this file CustomConfig.xcconfig
2) Declare overridable settings
This file should declare the settings we’d like to override but shouldn’t include the final values. This will allow us to safely commit it to the repository.
// Overridable settings with some defaults
PRODUCT_BUNDLE_IDENTIFIER = com.mydomain.myapps.AppName
DEVELOPMENT_TEAM = ""
CODE_SIGN_STYLE = Manual
// Optionally include `LocalConfig.xcconfig`
// that includes local overrides if it exists.
// `#include?` is supported as of Xcode 8
#include? "LocalConfig.xcconfig"
#include
allows pulling in settings from other configuration files, a warning will be generated if the referenced file doesn’t exist.
#include?
was introduced in Xcode 8 and similarly pulls in settings from other configuration files, but will not generate a warning if the referenced file is missing.
Pro Tip: If you want to figure out the setting name, simply select the corresponding build setting line in Xcode, copy (cmd+c) and paste it in the xcconfig file
3) Create a local configuration file
In finder duplicate the configuration file under a different name (e.g. LocalConfig.xcconfig
) without adding it Xcode. This is the file that will host our final values for the specified settings.
// Local Config
//
// - Update the values below as needed for your specific developer account
// - Avoid keeping the same IDs as it will clash with other developers
// - Remember to add this file to `.gitignore`
PRODUCT_BUNDLE_IDENTIFIER = com.myOtherDomain.myApps.MyOtherAppName
DEVELOPMENT_TEAM = ABCDEF
CODE_SIGN_STYLE = Automatic
To ensure this file remains local, add it to .gitignore
echo "LocalConfig.xcconfig" >> .gitignore
See gitignore documentation for more details on how that works.
4) Link the config files with the Xcode project
Within the info tab of the project, under the configuration section assign CustomConfig
to the project for both Debug & Release.
5) Clear any project or target setting overrides
You may notice some settings specified within the configuration file not taking effect. This could be due to having overrides specified at the project or target level.
To investigate further, navigate to the build settings tab and select the Levels view. This view will show the final resolved setting value and where it originated from.
In the event there’s a value specified at the target or project level for the setting in question, select it and simply hit backspace or delete to clear the override. The resolved setting should then reflect the value specified in the configuration file.
Entitlements
A few additional settings like which CloudKit
container to use aren’t part of the project build settings, but can be found within the entitlements file.
By default Xcode attempts to create and use a container with an id iCloud.$(CFBundleIdentifier)
. This should work in our case without further modification as one of the local overrides that we have already specified is a different bundle identifier. As such, we will end up with different container IDs that don’t clash.
However in the event an explicit ID is needed, a custom setting can be added to the configuration file (e.g. CLOUDKIT_CONTAINER_ID
) and can then be used in the entitlements file.
// CustomConfig.xcconfig
// ...
CLOUDKIT_CONTAINER_ID = iCloud.$(CFBundleIdentifier)
// LocalConfig.xcconfig
// ...
CLOUDKIT_CONTAINER_ID = iCloud.com.mydomain.myapps.AppName
Note : Be careful when specifying explicit IDs like the CloudKit
container ID and committing it into a shared repository. A different developer working on the same project can accidentally reserve your container ID before you!
What about CocoaPods?
CocoaPods by default will link its own configuration files to your project when used. Specifying our own custom configuration files is still possible, it just requires a few extra steps.
We need to ensure we reference the CocoaPods configuration files within our own using #include
. A clean way to do so is to create another configuration file that links both, the CocoaPods one as well as our own and linking that to our project.
e.g.
// Debug.xcconfig
#include "../Pods/Target Support Files/Pods-MyApp/Pods-MyApp.debug.xcconfig"
#include "CustomConfig.xcconfig"
See this useful Guide by Julia Geist for details.
Conclusion
This post only covered a handful of settings, the same technique can however be applied to many other settings that may need to differ locally or even remotely on a build server. Using configuration files in this capacity isn’t a new or revolutionary idea, it’s just another use case worth highlighting!