Flavorios - App Flavors in iOS!

Posted by Grego on March 14, 2017

I previously wrote an article about creating app flavors on Android and I had planned on writing another about how to do the same on iOS.

While Android already has a built in mechanism for creating what Google calls App Flavors, iOS unfortunately does not. However, we can tweak the Xcode build tools to do something similar.

Achieving Flavors in iOS

Goals:

  1. Allow multiple versions of our app loaded on the phone side by side
  2. Distinguish between one version or the other
  3. Allow us to make different decisions depending on flavor.

I think these are all self explanatory except for maybe #3. When developing on a multi-stage environment it may be desirable to, for example, point our app to a different API server for development builds than for production builds.

Another example would be a paid app vs a free app, wherein the free flavor will show annoying ads and the pay flavor will remove them.

This decision making can be done using Objective-C’s preprocessor and a similar mechanism in Swift called conditional compilation blocks.

Conditional compilation blocks don’t give quite the same functionality as a preprocessor but they give us just enough to do what we want.

We will use Xcode’s user-defined [build] settings and build configurations to get what we want.

Create Build Configurations

To keep things simple for now we will change our build configurations so that we will have Dev, Staging and Production configurations.

Open up your Project settings

Xcode Project Settings

Duplicate the Debug configuration, and name it Staging. Rename your other configurations accordingly.

Xcode duplicate Debug configuration

Note: I’m making a simple case and using 2 Debug configurations (Dev, Staging) and a Release configuration (Production) but ideally you’d have a debug and release configuration available for each flavor. This case isn’t demonstrating flavors as much as different environments, which may be a separate dimension from flavor.

Adding Build Settings

Switch over to the Build Settings tab.

Let’s create some user-defined settings. For clarity I like to prefix my custom user settings. I’m using OBF for One Big Function, the name of this site. Update yours accordingly.

For the example I’m using OBF_ENVIRONMENT.

If your app comes in different flavors like lite or pro (paid vs free) then you might want to add something like FLAVOR also.

Open the disclosure triangle and set up your environment and/or flavor to their appropriate values for each build configuration:

Xcode environment settings with values

  • Dev gets a value of DEV
  • Staging gets a value of STAGE
  • Production gets a value of PROD

Allowing Multiple Versions of the App Side By Side

To allow multiple versions of the app we need to change the app’s Bundle Identifier for each flavor.

Changing the bundle id will have certain side effects when it comes to code signing and provisioning profiles. The trick to this is to have more provisioning profiles created later with the new bundle ids.

I like to keep things separate because I think it’s a little bit cleaner, so I’ll add another user-defined settings called OBF_BUNDLE_ID_SUFFIX. I set the values according to their environments (or flavors):

Xcode bundle id suffix

I set the value to .$(OBF_ENVIRONMENT:lower) except for production which I left blank. The :lower produces a lowercase version of my $(OBF_ENVIRONMENT) setting, the result is:

  • Dev gets a value of .dev
  • Staging gets a value of .stage
  • Production gets a blank value because I don’t want any extra suffix when I submit my app to the store.

Don't forget to add the . in front of the $(OBF_BUNDLE_ID_SUFFIX)

Add $(OBF_BUNDLE_ID_SUFFIX) to the end of your Product Bundle Identifier and you can now have multiple version of the app side-by-side!

Xcode bundle id with suffix

Making the Settings Available to Code

To make the settings we just created available to code we’ll add them to the Active Compilation Conditions (Swift only) on the same screen.

Xcode's Active Compilation Conditions with our new
environment

In my example I added ENV_$(OBF_ENVIRONMENT) to the list. I can now use these values in swift using a compilation condition:

#if ENV_DEV
print("I'm in dev!")
#endif

For Objective-C projects we can specify these flags under the Preprocessor Macros setting.

Xcode Preprocessor Macros for Objective-C
Projects

In my example I added ENV_$(OBF_ENVIRONMENT)=1. The =1 is necessary for setting flags as this evaluates to true in our preprocessor checks:

#ifdef ENV_DEV
NSLog(@"I'm in dev!");
#endif

Whether you use that ENV_ prefix or not is entirely up to you but it may be cleaner in the long run if you end up with a lot of compilation conditions.

A More Practical Code Snippet

Since in my whole example I’m only using the concept of app flavors to change environments, let’s see a quick example of how we can accomplish this. First, in swift:

enum kUrl {
  static var baseUrl: String {
    #if ENV_DEV
    return "https://url.dev"
    #elseif ENV_STAGE
    return "https://url.stage"
    #elseif ENV_PROD
    return "https://url.com"
    #endif
  }

  static let users = "\(baseUrl)/users"
}

// now the urls can be accessed from the `kUrl` enum, as follows:
print("Base URL: \(kUrl.baseUrl)")

You will notice I didn’t include a catch-all #else. This was intentional. These are compilation conditions. This means that if there is no else, the build will fail, which is exactly what I want. I want it to be called to my attention that the string has not been set, rather than wondering why I am getting unexpected results from my app at runtime. This works because it will cause the app to fail early and it won’t even run, so end users will never encounter this scenario.

Objective-C would be something similar:

#ifdef ENV_DEV
static NSString *const kUrlBase = @"https://url.dev";
static NSString *const kUrlUsers = @"https://url.dev/users";
#elif ENV_STAGE
static NSString *const kUrlBase = @"https://url.stage";
static NSString *const kUrlUsers = @"https://url.stage/users";
#elif ENV_PROD
static NSString *const kUrlBase = @"https://url.com";
static NSString *const kUrlUsers = @"https://url.com/users";
#endif

// ...

NSLog(@"Base URL: %@", kUrlBase);
NSLog(@"Users URL: %@", kUrlUsers);

In the Objective-C version I took a different approach instead of using concatenation since this is a hairy area in general. It makes updating the base URL more tedious but gives the flexibility of having radically different URLS everywhere. Concatenation using [NSString stringWithFormat:] would require us to do so at runtime, with a function of some sort.

Distinguishing One Flavor From Another

Now that we have different versions of the same app on the device, they all look very similar, which is problematic.

Depending on how you use flavors, the answer might be obvious — just change the name of the app!

Both app icon and app display name can be modified per-flavor, again using the Build Settings from before.

Changing App Name Per Flavor

Open the Build Settings for your target. This is similar to the project build settings, except you have to click on the target instead of the project.

Look for Product Name the default value at time of writing is $(TARGET_NAME), let’s change this to use our environment name, with the exception of production where we will continue to use $(TARGET_NAME).

Different product names per flavor in Xcode

If your product names are all blank chances are you're in the wrong place. Make sure it's under the target's build settings.

If your app has a totally different name based on flavor, you can just type it right in here.

In my example, I used $(OBF_ENVIRONMENT:lower) again, with the exception of production where I named the app appropriately.

Changing App Icon Per Flavor

Here’s an example from a separate sample project I made to show you how you can dazzle things up using emoji to differentiate things like environments or debug vs release builds.

App flavors on the home screen

To change the App Icon per flavor, we can again edit our mighty Build Settings on the target level. In my case I’ll change the Asset Catalog App Icon Set Name from AppIcon to AppIcon$(OBF_BUNDLE_ID_SUFFIX)

Xcode app icon set name

This attaches the bundle id suffix to the name, resulting in:

  • Dev uses AppIcon.dev
  • Staging uses AppIcon.stage
  • Production is uses the default AppIcon.

In order for this to work we need to create 2 more iOS App Icon sets in our Assets.xcassets and rename them accordingly.

Adding a new iOS app icon in xcode

Drop your images in and you’re done!

Switching Flavors

To switch between flavors you can either duplicate your build scheme or use the same scheme and switch by Option + Click-ing on the scheme at the top and switching the build configuration from the drop down.

Changing build configurations in Xcode