Passing Build Settings Into Property Lists

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.

Screenshot of Xcode

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.

Screenshot of Xcode

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.

Screenshot of Xcode

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.

About The Author

Peter Thomas Horn is a professional software developer at Open-Xchange specialized on the Apple platform. He previously worked a decade across the full stack of various web technologies. Originally started with Java on Windows at the age of 12 years. While staying humble in throwing around buzzwords like "VR" and "machine learning" he occasionally experiences problems and the fitting solutions considered worth sharing.