Xcode file variants without targets
Build Rules to select a file variant
If you want to configure your iOS/Mac (Swift or Objective-C) build with some alternative variant, there are plenty of different approaches to follow. In general, you can divide them into two clusters: switch it in runtime (e.g. parsing Info.plist
) or compile-time (leveraging Swift Compilation Conditions/Preprocessor Macro or define new target). Obviously, we always prefer the compile-time check but the overhead often may overweight its benefits.
This post will present an alternative way to control a file variant selection based on Xcode Build Rules (in contrast to traditional Xcode targets).
Problem statement
There are plenty of solutions to build a different flavor of the app that may differ in:
- endpoint address (e.g. staging vs production)
- logic (e.g. enable/disable geofencing check)
For more details, let me recommend this post, which presents most of them in details.
Long story short, for Swift development you can choose between:
- Compilation Conditions to use
#if DEBUG ... #endif
structure to include or disable given block of code - separate targets, where each target contains a separate, dedicated file with a specific code for given configuration
- runtime checks from
.plist
input file or environment variable
Each has a drawback, to mention:
- iffing on compilation conditions may lead to a messy code with a plethora of
#if
that affects the readability - separate targets introduce the unnecessary need to include all the “shared” codebase and configuration to all of them
- for runtime configuration, we lose compile-time check.
Wouldn’t be great if we could have a solution with a single target that depending on a configuration uses a specific variant of a file?
Solution: Use or skip a .swift
file using custom Build Rule
Traditionally, Xcode targets give you a chance to selectively choose which file to include for compilation. You may leverage it to replace one source file with another one that specifies other baseURL
or completely different logic strategy. For the sake of this post, let’s assume that we want to use one of two different Configuration_X.swift
files:
// Configuration_S.swift
struct Configuration {
let baseUrl = "https://staging.example.com/"
}// Configuration_P.swift
struct Configuration {
let baseUrl = "https://example.com/"
}
Without targets, we can achieve that using custom “Build Rules”, less popular tab in Xcode’s project, where you can specify how to process project files depending on their filename.
When adding a custom Build Rule, you have to specify file pattern, a shell script to apply and what is the output file of your script. Then, Xcode during a compilation process will evaluate your script for files that match given pattern instead of a default behavior (e.g. compiling .swift
files). Please keep in mind that custom Build Rules have a precedence over the embedded rules — that gives us a chance to override the default compilation behavior.
For our solution, we will follow the algorithm:
- include all versions of
Configuration_X.swift
into a single target - Xcode project specifies a dummy Build Rule that swallows files you want to skip (exclude from a build)
- all other files (out of
xxxxx_x.swift
format) are processed as usual
To control which files to swallow in a dummy Build Rule we will create a separate Xcode Configuration with User-Defined Setting CONFIGURATION_VARIANT
(the setting name is of course up to you):
Above configuration means that for “Debug” configuration we would like to choose staging file variants with “S” postfix (xxxxx_S.swift
) and for “Release” production with "P” postfix (xxxxx_P.swift
).
There is plenty alternative options to specify User-Defined Build Setting, e.g. `buildsetting=value` argument for xcodebuild terminal command.
Coming back to our newly created Build Rule, let’s specify that we want to manually process a subset of the project’s .swift files:
- Process Swift files that end with a single-character postfix other than
CONFIGURATION_VARIANT
(please note that Xcode uses simple pattern matching rather than more powerful regular expressions here) - The script creates in a derived directory an empty file with
_skipped
postfix - Specify that Xcode should process just created empty file instead of an original one — here we technically consider the body of a file as empty.
Please note that we operate on a
$DERIVED_FILE_DIR
directory so given script creates a file in a build directory and doesn’t modify an original file, potentially under source control.
As a result: no more multiple-targets, no more #if... #endif
blocks, no runtime checks— Build Setting (e.g. specific for a Configuration) controls if a given file should be included or not.
A sample app that demonstrates it is available on GitHub.
Finally, please note that this technique isn’t limited to .swift
files. With a small modification of a Build Rule presented above, you may apply it for Objective-C files, images, assets or other data like .json files too.
Summary
Build Rules is a tool often underestimated and barely used by iOS/Mac developers. This post demonstrates how it can be useful to selectively include/exclude .swift
files from a compilation process. Now we have another alternative to consider before immersing to the world of multiple-target projects or conditional-based code.
One limitation is that we can use only single-character postfix marker (like _A.swift, _B.swift, _1.swift etc.) as Xcode does not support glob or regex file matching in Build Rules tab. Feel free to duplicate OpenRadar suggestion that gives Build Rules more control on that.