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.

Adding custom Build Rule

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.

Custom Build Rule configuration screen

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
Overview of the file variant selection (Release build)
Overview of the file variant selection (Debug build)

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:

  1. 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)
  2. The script creates in a derived directory an empty file with _skipped postfix
  3. 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.