Passing Build Settings Into Property Lists
• 4 minute read
An app which is customizable for different brands and customers also requires a parameterized build configuration. Some values need to be accessible during runtime, too, while retaining a single source of truth. This is more elegant take on what could have been achieved with preprocessor macros in Objective-C times.
Working on an app which is customizable poses some additional requirements one does not have to care about otherwise. Specifically the different developer teams and app records require the signing to be set up flexibly. Sometimes it is about simpler aspects like a server URL which is specific to each customization. Xcode build settings can be overridden by environment variables in a CI environment. This combination provides an opportunity.
Why?
You may ask: Why not use the Info.plist
which is part of every app and automatically processed by default?
The simple matter of a clean separation between predefined property lists required by the platform and custom implementation code.
The essential app info dictionaries are not littered and customization configurations remain concise and clear at a glance.
From my experience it also happens too often that obsolete entries clutter up the manifest because they simply are forgotten.
Obviously this is not a strong argument.
Maybe even just a matter of taste.
And, if it is about only a single entry, it is more pragmatic to just put it into the Info.plist
.
But if customizations are extensive, this might be a neat alternative.
Customization Property List
Let’s assume our app is customizable and for the sake of this example has only the serverURL
property.
That must be stored in a Customization.plist
(or however you would like to name it).
Pay attention to the target membership!
This causes the file to be copied into the app bundle as a resource during build automatically.

The value can be left empty because it will be defined during build time based on Xcode build settings.
To have a clear demonstration of this process to work the value can also be defined as shown above: https://placeholder.example.org
.
Build Settings
The replacement value must be defined somewhere.
Typically, the arguments for a parameterized build configuration are defined in build settings files having the *.xcconfig
extension.
I created a Configuration.xcconfig
in the project without it being part of a build target.
Even though it is there, it is not considered automatically by Xcode.
It has to be referenced explicitly in the build configurations of the project or targets.
There one can select it in the “Based on Configuration File” column.
In scope of this example it is sufficient to simply select it for the whole project because we only have a single build target anyway.

The content of this file is fairly simple:
CUSTOMIZATION_SERVER_URL = $(CUSTOMIZATION_SERVER_URL:default=default.example.org)
It defines the new CUSTOMIZATION_SERVER_URL
variable with a environment variable evaluation and optional default value.
So, if the environment in which Xcode builds defines this variable already its value will be used.
Otherwise, the default value written in here will be used.
This is handy to have projects build with the default value locally and use a different one in a CI pipeline.
Build Phase
Next we have to set the actual values in the Customization.plist
during the build.
For that we create a new “run script” build phase for the app target, after the resources have been copied.

There the PListBuddy provided by macOS can be used to set property list values in a command-line environment. Build settings are available in form of environment variables in those scripts. In case of this example it is only this one line:
/usr/libexec/PlistBuddy -c "Set serverURL '$CUSTOMIZATION_SERVER_URL'" "$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/Customization.plist"
After building the app you can inspect the produced app bundle and contained Customization.plist
for the value of $CUSTOMIZATION_SERVER_URL
to be set.
Retrieval
Finally, as a gimmick, here is how to retrieve it during runtime then:
struct ContentView: View {
var serverURL: String {
guard let customizationPropertyList = Bundle.main
.url(forResource: "Customization", withExtension: "plist") else {
assertionFailure("Failed to find property list!")
return ""
}
guard let data = FileManager.default.contents(atPath: customizationPropertyList.path) else {
assertionFailure("Failed to read property list!")
return ""
}
guard let dictionary = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else {
assertionFailure("Failed to deserialize property list!")
return ""
}
guard let serverURL = dictionary["serverURL"] as? String else {
assertionFailure("Failed to retrieve server URL!")
return ""
}
return serverURL
}
var body: some View {
Text("This app connects to \(serverURL)")
.padding()
}
}
Of course this only is a proof of work, hacking it into a SwiftUI view is not the ideal way. I advise against using it this way in actual production code. Usually I make such customization properties accessible through a convenient, tested and type-safe API which takes care of errors.
Conclusion
This implementation keeps things neatly separated and is a building block in a parameterized build configuration which enables new, customized apps by just building with different input arguments. The pinnacle would be a simple web form where a user can fill in some customer information and a ready-to-use app drops out afterwards.
I created an example Xcode project which is available on GitHub. At the time of writing it offers a working example implementation of the process described above.