Flavors in iOS – part 1

Tired of working with hashtags all around? Bored of having duplicated code just to have the same app but with different styles? Keep reading to have a clean and good looking solution for it. Welcome to iOS Flavors!

Background

On many occasions, we have clients or projects that need to have several apps. And actually those apps are not more than small variations of the same source code.

There has always been a big gap between iOS and Android app development about having different styles of the app. App Flavors itself is an Android concept; on iOS, we do not have that by default, but we DO HAVE THE NEED for it.

In this tutorial we will see how to configure our project to have flavors in a simple and elegant solution.

Initial Point

We have created an initial project fully prepared in our git repository that you can find here. You can do a git clone and follow this step-by-step tutorial with me.

We also have another repository with the final solution. Clone it in case you want to have the solution while you are reading the tutorial.

How to approach the issue

To carry out the initial structure of our project, we have to tackle four significant milestones: 

Targets

Apple define a target as follows

A target specifies a product to build and contains the instructions for building the product from a set of files in a project or workspace

In purely execution terms, we are going to make a one-to-one relationship between target and flavor.

So, we have to create a Target for each flavor that we want to add to our project. When we need to run one or another app, we will select which target we want to and run it.

How to create a new Target:

Duplicate the current target that we will find in our initial project.

Rename each target with the names that we want to add to each flavor. We are going to work in this example with two flavors:

Rename each info.plist file. In our example will be:

Move both .plist file to /SupportingFile group

Link each .plist file to each target

Add a custom flag to each target. At:

Now that we have created the targets, we will continue with the flavor definition.

Flavor definition

We are going to define what a flavor is for our app.

A flavor should not be more than a variable that will say (in execution time) how our app will appear.

For that, we will start defining an enum where we are going to list all our flavors.

// FlavorTarget.swift

enum FlavorTarget: String {
    case superman
    case batman
}

To be reachable in the whole app, we will add a static variable of this enum at AppDelegate. So the target can be identified in execution time.

It will be the only place in the whole app where we will use the # (hashtags). This variable will leave our app (almost) free of hashtags.

// AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    static var flavor: FlavorTarget {
        #if BATMAN
        return .batman
        #elseif SUPERMAN
        return .superman
        #else
        fatalError("Target flag incorrectly specified")
        #endif
    }

    func application(
        _ application: UIApplication, 
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) 
        -> Bool {
        
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(
        _ application: UIApplication, 
        configurationForConnecting connectingSceneSession: UISceneSession, 
        options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        return UISceneConfiguration(
            name        : "Default Configuration", 
            sessionRole : connectingSceneSession.role)
    }

    func application(
        _ application: UIApplication, 
        didDiscardSceneSessions sceneSessions: Set) {
        
    }
}

Our project now has a target per flavor, and also, the app can now know which flavor we are running each time.

Now is time to define the colors that we will need to customize.

Flavor colors

Now is time to define our flavor’s color scheme.

For that, we are going to add a .xcassets file to each flavor where we define our colors. 

At the project structure tree we are going to create a new group inside Resource. This group will be called Colors. In here we do: Right click → New File… we select Asset Catalog and then call it with the same target/flavor name. Be 100% sure that you only select the correct Target. So Superman.xcassets is only for Superman’s target and Batman.xcassets is only for Batman’s.

Colors schemes for our example.

Colors R G B
Apple Green 127 184 0
Caroline Blue 0 166 237
Chinese Yellow 255 180 0
Prussian Blue 13 44 84
Red Orange Wheel 246 81 29
Colors R G B
Cafe Noir 83 62 45
Camel 184 139 74
Flax 221 202 125
Golden Brown 162 112 53
Raisin Black 36 35 49

Now we are going to integrate those colors with flavor style into the UIKit library, to make it easier to reach from the code. 

Define FlavorColor protocol. This protocol will define a name (color asset name) and a value (an instance of UIColor object with the color itself):

// FlavorColor.swift

import UIKit

protocol FlavorColor {
    var name  : String  { get }
    var value : UIColor { get }
}

Create UIColor+Extension where a new contractor will be defined. This new constructor will use a FlavorColor as input, so by naming the flavor color we will obtain an UIColor object.

// UIColor+Extension.swift

import UIKit

extension UIColor {
    convenience init(flavorColor: FlavorColor) {
        self.init(named: flavorColor.name)!
    }
}

To complete this we need to implement our new protocol. For that we are going to add one enum for each flavor that implements it. Those enums have to be a String enum. 

Each one will contain all the color list available for each flavor. I have added this to the FlavorColor.swift file.

The String value has to have the same name as the color specified at xcassets file.

// FlavorColor.swift

import UIKit

protocol FlavorColor {
    var name    : String    { get }
    var value   : UIColor   { get }
}

enum BatmanColor: String, FlavorColor {
    case cafeNoir       = "cafeNoir"
    case camel          = "camel"
    case flax           = "flax"
    case goldenBrown    = "goldenBrown"
    case raisinBlack    = "raisinBlack"
    
    var name: String {
        return self.rawValue
    }
    
    var value: UIColor {
        return UIColor(flavorColor: self)
    }
}

enum SupermanColor: String, FlavorColor {
    case appleGreen     = "appleGreen"
    case carolineBlue   = "carolineBlue"
    case chineseYellow  = "chineseYellow"
    case prussianBlue   = "prussianBlue"
    case redOrangeWheel = "redOrangeWheel"
    
    var name: String {
        return self.rawValue
    }
    
    var value: UIColor {
        return UIColor(flavorColor: self)
    }
}

Update UIColor+Extension with the most common elements that you have to customize in your app.

// UIColor+Extension.swift

import UIKit

extension UIColor {
    convenience init(flavorColor: FlavorColor) {
        self.init(named: flavorColor.name)!
    }
    
    static var background: UIColor {
        switch AppDelegate.flavor {
        case .batman:
            return .systemBackground
        case .superman:
            return .systemBackground
        }
    }
    
    static var buttonTint: UIColor {
        switch AppDelegate.flavor {
        case .batman:
            return BatmanColor.camel.value
        case .superman:
            return .white
        }
    }
    
    static var tabBar: UIColor {
        switch AppDelegate.flavor {
        case .batman:
            return BatmanColor.raisinBlack.value
        case .superman:
            return SupermanColor.prussianBlue.value
        }
    }
    
    static var tabBarSelectedItem: UIColor {
        switch AppDelegate.flavor {
        case .batman:
            return BatmanColor.camel.value
        case .superman:
            return SupermanColor.chineseYellow.value
        }
    }
    
    static var tabBarUnselectedItem: UIColor {
        switch AppDelegate.flavor {
        case .batman:
            return BatmanColor.goldenBrown.value.withAlphaComponent(0.5)
        case .superman:
            return SupermanColor.chineseYellow.value.withAlphaComponent(0.5)
        }
    }
    
    static var navigationBar: UIColor {
        switch AppDelegate.flavor {
        case .batman:
            return BatmanColor.cafeNoir.value
        case .superman:
            return SupermanColor.carolineBlue.value
        }
    }
    
    static var navigationBarTitle: UIColor {
        switch AppDelegate.flavor {
        case .batman:
            return .white
        case .superman:
            return .black
        }
    }
}

On this point we have created a target to each flavor and we have implemented and integrated our flavor into the UIKit library. We have to take the last step to have our solution 100% implemented.

Customised View Controller

To integrate the flavor functionality into the whole app, we will create an open UIViewController class. Then this class will be inherited for all our View Controllers.

We guarantee that the customization of all the elements that need to be done is done correctly. We keep our View Controllers free to implement the functionality, which is what interests us. 

We can always override the UIViewController methods, in case some view controllers have special UI needs.

// FlavorUIViewController.swift

open class FlavorUIViewController: UIViewController {
    override open func viewDidLoad() {
        super.viewDidLoad()
        self.initView()
    }
    
    // MARK: - UI
    func initView() {
        self.view.backgroundColor = .background
        self.initNavigationBar()
        self.initTabBar()
    }

    func initNavigationBar() {
        self.navigationController?.navigationBar.barTintColor = .navigationBar
        self.navigationController?.navigationBar.titleTextAttributes = [
            .foregroundColor : UIColor.navigationBarTitle
        ]
    }

    func initTabBar() {
        self.tabBarController?.tabBar.barTintColor = .tabBar
        self.tabBarController?.tabBar.tintColor = .tabBarSelectedItem
        self.tabBarController?.tabBar.unselectedItemTintColor = .tabBarUnselectedItem
    }
}

Now we can create our View Controllers and inherit FlavorUIViewController instead of UIViewController. This approach reduces repeated code across view controllers, and also reduces the complexity of these by moving (part of) the UI configuration to a separate class.

Here we have an example about how to use it

// FavoritesViewController.swift

import UIKit

class FavoritesViewController: FlavorUIViewController {
    override func initNavigationBar() {
        super.initNavigationBar()
        self.navigationItem.title = "Favorites"
    }
}

Conclusions

Reaching this point, both flavors of the app can be installed. 

We now have two separate apps, with different characteristics but with the same source code.

Also, the app’s View Controllers are completely free of hashtags (#) and UI customization rules.