by guestwriter
Share
by guestwriter
Share
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, the way that xcode offers us to identify a single product.
- Flavor definitions, a simple solution designed to let the target be identified without hashtags all around.
- Flavor colors, integration of the flavor concept directly in the UIKit library.
- Customised View Controller, an elegant solution to let the customization be totally transparent for our controllers.
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:
- Batman
- Superman
Rename each info.plist file. In our example will be:
- Batman-Info.plist
- Superman-Info.plist
Move both .plist file to /SupportingFile group
Link each .plist file to each target
- Select the target at project view
- Go to Build Settings tab
- Select the view Basic and Combined
- At Packaging section, change the Info.plist File to the correct location
Add a custom flag to each target. At:
- Project (select the target) → Build settings
- Select the view Basic and Combined
- At the searcher bar type “swift flags”
- At Swift Compiler – Custom flags → Other Swift Flags, add
- -DBATMAN for Batman flavor and
- -DSUPERMAN for Superman flavor.
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 for Superman flavor:
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 for Batman flavor:
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.
STAY IN THE LOOP