Configuration files in a multiple environment app

Helder Pinhal
Helder Pinhal
Dec 17 2021
Posted in Engineering & Technology

Leverage Build Phases to load the right configuration.

Configuration files in a multiple environment app

Nowadays it's quite common for apps to support multiple environments. Whether it's a simple development & production setup, or a more complex scenario where a white-label app supports multiple customers / variants, we always bump into the question:

How can we have different configurations for each variant?

A common approach is to use User Defined Attributes through the Build Settings, which allows you to specify a setting for each scheme. This is a good approach, it is a great and simple way to let the build process decide the appropriate value for your setting, which can be read by the app when added to the Info.plist.

In order to write the setting to the Info.plist, we can do something like the following:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>SERVER_URL</key>
	<string>$(SERVER_URL)</string>
</dict>
</plist>

From this moment on, we can read the setting via code:

let serverUrl = Bundle.main.infoDictionary?["SERVER_URL"] as? String

While this solves the problem of a specific setting, this is hardly enough to manage a configuration file like the GoogleService-Info.plist or our very own NotificareServices.plist. These files are scoped to a specific application, which means, if your source code supports multiple apps, it's most likely you will several of these files.

Perhaps we could still leverage this solution. The aforementioned tools do support configuration through code, providing the appropriate file. Considering we're using the Notificare v3 SDK, we could do something like:

let variant = Bundle.main.infoDictionary?["VARIANT"] as? String

let servicesInfo = NotificareServicesInfo(contentsOfFile: "NotificareServices-\(variant).plist")
Notificare.shared.configure(servicesInfo: servicesInfo)

This assumes a certain file structure — NotificareServices-*.plist — so we would have to include all of the configuration files in the appropriate format. The solution works, however, the built app will also include all of them. If you are using multiple targets, it's possible to only include the relevant files in each target, but that usually causes more issues than it solves.

A nice alternative is to leverage Xcode's Build Phases where we can, during the build process, decide which is the appropriate configuration file to include in the build.

The first step is to add all the configuration files to the project but do not include them in the target!

Proceeding into the Build Phases, add a Run Script Phase and make sure it runs before Copy Bundle Resources. This little scripts needs to be aware of where the files are stored (in the project) and where they should be packaged. Here's that bit of magic:

# Name of the resource to copy
INFO_PLIST_FILE=NotificareServices.plist

# Get references to debug and release versions of the plist file
DEBUG_INFO_PLIST_FILE=${PROJECT_DIR}/${TARGET_NAME}/Resources/Notificare/Debug/${INFO_PLIST_FILE}
RELEASE_INFO_PLIST_FILE=${PROJECT_DIR}/${TARGET_NAME}/Resources/Notificare/Release/${INFO_PLIST_FILE}

# Make sure the debug version exists
echo "Looking for ${INFO_PLIST_FILE} in ${DEBUG_INFO_PLIST_FILE}"
if [ ! -f $DEBUG_INFO_PLIST_FILE ] ; then
    echo "File NotificareServices.plist (debug) not found."
    exit 1
fi

# Make sure the release version exists
echo "Looking for ${INFO_PLIST_FILE} in ${RELEASE_INFO_PLIST_FILE}"
if [ ! -f $RELEASE_INFO_PLIST_FILE ] ; then
    echo "File NotificareServices.plist (release) not found."
    exit 1
fi

# Get a reference to the destination location for the plist file
PLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app
echo "Copying ${INFO_PLIST_FILE} to final destination: ${PLIST_DESTINATION}"

# Copy the appropriate file to app bundle
if [ "${CONFIGURATION}" == "Debug" ] ; then
    echo "File ${DEBUG_INFO_PLIST_FILE} copied"
    cp "${DEBUG_INFO_PLIST_FILE}" "${PLIST_DESTINATION}"
else
    echo "File ${RELEASE_INFO_PLIST_FILE} copied"
    cp "${RELEASE_INFO_PLIST_FILE}" "${PLIST_DESTINATION}"
fi

In this case, we have two different files — for the Debug and Release schemes — which is a pretty simple setup. However, if we're following the multiple variants scenario, we can easily modify the script to use a User Defined Setting instead of the build configuration. Since those variables are exposed to the scripts, we can use it like ${VARIANT}.

Do note the paths in the script above imply a folder structure like the following, nested under a directory named after the target:

Conclusion

The great part about this technique is that only one configuration file will be embedded in the app regardless of how many files / variants we may have. Additionally, since it's an automated and tested process, it's going to prevent the good-old-mistake we, developers, make — forgetting to toggle the appropriate configuration.

As always, we hope you liked this article and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news