Using XCode Build Targets To Manage Your Deployments

Over the last while I’ve had to start managing some mobile projects that had a fair amount of complexity to them. Whether its to release a feature-reduced version, a version for testers, or supporting development processes like Gitflow, there’s a lot of situations that come up in development that require configuration change - change that is better handled at compile-time than run-time.

I’ve watched a lot of configuration change happen manually, and it makes me nervous. We’ve all known coders that insist on using VI and GCC to write their software, but tools like Xcode aren’t just for syntax highlighting and file organization; they’re designed to reduce the complexity of projects, mitigate risk - its about letting us work smarter, not harder.

I’ve been using a combination of targets and preprocessor macros to manage my different app builds such as production and user-testing. Its worked out really well for me so far. I’m going to assume absolutely nothing in this article, since new iOS developers are likely to get the most out of it.

The Lingo

First lets cover a few terms and concepts in the name of consistency:

Build Target: A defined product of work to be created by the build system; a library, an executable, etc.

Build Configuration: A collection of settings that instructs the compiler tools on how to perform the work required to produce the build target.

Build Scheme: An organizational unit comprised of one or more targets and a build configuration.

When you initialize a new Xcode project, it provides you with a Build Schema with two Build Configurations: Release and Debug. It will also include a single Target, your app, unless you specified the project to include tests.

Creating Targets

Targets are commonly used to build dependencies for projects, but they come in handy for building different configurations of an app. For example, I always set up a separate “Test” app Target for my app that integrates TestFlight and alters analytics reporting.

  1. Create duplicate of existing target (easier than creating a new one)
  2. Each target has its own Info.plist; a new one will be created, update its name
  3. Edit the properties for the new target, and confirm the new settings:
    1. Build:
    2. Packaging -> Info.plist filename
    3. Packaging -> Product Name
    4. Properties:
    5. Icon File
    6. Version

Using Preprocessor Macros

One of the ways you can use the preprocessor is to create conditional logic (“directives”) that specifies code that can be included/excluded before it reaches the compiler. A simple example of this is the DEBUG flag that is set by default for the Debug Build Configuration, but in our example we define a new flag for use with TestFlight.

  1. Under Build Settings go to the section labelled Preprocessor
  2. Expand the field labelled Preprocessor Macros
  3. Double click on the Build Configuration you wish to add a flag to and enter the value

Once the macro is in place, we add some preprocessor directives to make sure our TestFlight initialization only runs if that flag is defined:

#ifdef TESTFLIGHT
    [TestFlight takeOff:@"APP_TOKEN_GOES_HERE"];
#endif

Resources

You can assign resources (such as images) to a particular Target as well. Pro-tip: when working with multiple targets, make sure the resources included in each target bundle are actually required.

In our example of setting up a Test build, we can add a distinct app icon that visually differentiates the production/release version from the test version.

Simpler Builds

A little configuration up front eliminates a lot of hassle and risk in the long run. By defining Build Schemes with distinct purposes, we can quickly and easily spin off a build for a given audience with just a few clicks. It gets even better when you move to a continuous deployment and automated testing configuration - but I’ll save that for another article.