It common for all but the most trivial applications to conditionally compile parts of the code to enable/disable features or change configurations. Changing the server endpoint URL or enabling a feature to prominent show current version for QA only releases is quite common.
This can be easily done using Swift’s Active Complication Conditions or using preprocessor tokens in Objective-C. So as far as the source code we have a solution for conditionally including parts, but how about plist files, or in general other text resources? How can you run the preprocessor to any plist file so that you can write something like this
<key>DefaultValue</key>
#if FIREBASE_PERFORMANCE_COLLECTION_ENABLED==1
<true/>
#else
<false/>
#endif
Why run the proprocessor to Plist files?
It might not be obvious the usefulness to conditionally include parts of a plist but consider the following use cases:
- Many third party software have plist configuration files like Google Firebase has a GoogleService-Info.plist, you might have different configuration for each staging environments that only changes some IDs and the keys but you are forced to have multiple plist files in your project.
- If you want to add some extra options/information in the root.plist of the settings.bundle depending on the staging, perhaps you can give more parameters for the QA Team.
- Or whenever you have an plist file that you have to change (add/remove) parts depending on the staging environment.
Most solutions to the above problem consist of custom build phase scripts, that are running after the app is build and parse/manipulate the plist files inside the produced .app/. This has several downsides, it is an opaque process, it is not obvious to the developer that this processing is happening and it is difficult to see the final plist (you will have to open the .app, and some plists like the info.plist have by default binary format and are not easily viewable). Because this processing is done after the Xcode has copied and validated the copied plists, if the above script malfunctions and produces an malformed plist you will notice this latter when the application runs, not not at build time. If it malfunctions in a way that it does not produce an invalid output but simply does not performs its indented function then you might never notice in time. An other downside is that for each plist that you need to conditionally add/remove parts you will have to have a seperate script, not having a unified approach adds to the total complexity. So that is the solution?
Using Active Compilation Conditions and Clang Preprocessor to Plist files
To use the preprocessor to parse any plist file(or any text file) we can unifdef command line tools to run the preprocessor on that Plist. You just need to pass it all the preprocessor tokens in the form -DSOME_TOKEN
.
/usr/bin/unifdef -t -DSOME_TOKEN example.plist
<plist version="1.0">
<dict>
<key>SomeKey</key>
#ifdef SOME_TOKEN
<string>SomeValue1</string>
#else
<string>SomeValue2</string>
#endif
</dict>
</plist>
Before
<plist version="1.0"> <dict> <key>SomeKey</key> <string>SomeValue1</string> </dict> </plist>
After
This works great, but the unifdef does not process not defined tokens. So if in the above scenario instead of -DSOME_TOKEN
we had -DSOME_OTHER_TOKEN
then the output would be the same as the input. To remove all the undefined tokens we can run tune unifdefall command, after the unifdef. The unifdefall as the name implies removes all conditional directives.
/usr/bin/unifdefall example.plist
<plist version="1.0">
<dict>
<key>SomeKey</key>
#ifdef SOME_TOKEN
<string>SomeValue1</string>
#else
<string>SomeValue2</string>
#endif
</dict>
</plist>
Before
<plist version="1.0"> <dict> <key>SomeKey</key> <string>SomeValue2</string> </dict> </plist>
After
Parse Plists in you Xcode project
So in order to use that above in any Plist in your Xcode project you can add a build phase script that will have as input the a Plist with preprocessor directives and it will output a processed Plist that it will be include in the Xcode target.
To achieve perform the following:
- Rename it to <original name>-template.plist and remove it from any target. (Adding preprocessor directives will make it invalid plist so the default viewer will not opened it, so you can declare it as xml)
- Then add an other copy of the plist named <original name>.plist in the project and add it to the target and then delete it from the disk(will appear as red in the project indicating broken reference, no worries this will be produced by the preprocessing script bellow from the -template.plist).
- Now create a build phase script that processes any plist/xml(or any type of file) that we declare as input and produces as output the <original name>.plist. We can add as many input/output plist pairs are we need. The following script will do the preprocessing using the preprocessor tokens from the GCC_PREPROCESSOR_DEFINITIONS but you can use ACTIVE_COMPILATION_CONDITIONS for a Swift based project.
The build phase script should look something like something like this
Now you can open the <original name>-template.plist and add any preprocessor macros you want to conditionally include parts of that plist.
The Xcode build system will now see the plist file that is included in the target it is the output of the above script, so it will run it to generate it.
Conclusions
By having the above process you keep all the logic and your intent on how the final plists you want to look like all in one place. The Xcode build system knows that script is going to generate the plist and will call the build script phase to produce it when it is needed. The final plist that is going to be used is in the project navigation and easily viewable
If you have any error in the process and an invalid plist will is produced then when Xcode copies it, it will also validate and produce error in build time.