Mobile apps are often tied to backend services and are expected to evolve in parallel with them. Of course, the backend developers have it a lot easier. They can release a new version of their application pretty much anytime they wish. For many microservice frameworks, the ability to quickly push new API versions is a core feature. It's expected. iOS developers aren't this fortunate. Regardless of how quickly they can turn a release around, everything needs to go through the App Store and users need to update the application. Both of these factors are a constant drag on the release cycle. How can we work around these obstacles? With feature toggles. Feature toggles (also known as feature flags) give us a mechanism for adding new behavior without deploying a new release. We can even enable or disable the new functionality when we wish. So we add a new configuration parameter to our application, set it to "off," and then flip it on when we're ready, right? Well, there's more to it than that. Using feature flags to effectively manage change and improve your user's experience requires understanding feature flag strategies and tactics. In this tutorial, we'll add a feature toggle to a simple iOS app, first as a configuration parameter and then as a managed flag that will demonstrate how to avoid creating technical debt while adding the ability for your application to evolve quickly. This tutorial assumes familiarity with basic iOS concepts and the ability to set up and use Xcode. The source code for the complete project is available here.
A Basic Feature Toggle
We'll start with the single view iOS application offered in Xcode.
Then we'll add a UILabel to the application's single view.
Next, we need to define the label in ViewController.h with the name greetingLabel.
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (weak, nonatomic) IBOutlet UILabel *greetingLabel; @end
Finally, we'll set a value for the label in ViewController.m. Add the assignment in viewDidLoad.
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. _greetingLabel.text = @"Hello, World!"; }
Of course, setting the default value in the UILabel definition is another way to do this, but we'll define it explicitly throughout the tutorial to make the behavior easy to see. We're done setting up the sample application. Let's build it and run.
Now we can use a feature toggle to modify this application's greeting.
An iOS Feature Toggle (Objective-C)
Let’s imagine we want to replace our greeting with a different one depending on the season. The least common denominator is a boolean flag that toggles one string or another. So let's add some logic to how we set the greeting.
bool isNewYear = true; - (void)viewDidLoad { [super viewDidLoad]; if (isNewYear) { _greetingLabel.text = @"Happy New Year!"; } else { _greetingLabel.text = @"Hello, World!"; } }
Now build and run this version of our application.
We have our first feature toggle! But this means modifying, building, and getting a release through the App Store whenever we want to change our greeting. It's also a potential form of technical debt. What happens to these boolean flags down the road? Do they stay in the code, all set to true? Using a configuration file would at least shave a little bit of time and effort off the release cycle. We'll create a property list and control our toggle with a configuration property. Click your project's name and then click File|New in the main menu.
Click on property list and then Next.
Name the file as shown and click Create. Next, add a property name isNewYear.
We now have a property list named featureflag.plist. In it, there is one parameter: a boolean flag named isNewYear. We'll load it and use the property to set our flag. Rather than encapsulate the configuration code in another object or the AppDelegate, we'll load it right where we use the feature to keep things simple.
- (void)viewDidLoad { [super viewDidLoad]; NSString *path = [[NSBundle mainBundle] pathForResource:@"featureflag" ofType:@"plist"]; NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:path]; bool isNewYear = [[dict objectForKey:@"isNewYear"] boolValue]; if (isNewYear) { _greetingLabel.text = @"Happy New Year!"; } else { _greetingLabel.text = @"Hello, World!"; } }
Now we can toggle the value of isNewYear to YES or NO via the properties file and we'll see the proper greeting. The plist gets us out of modifying code when we want to toggle a feature, but we still need to build and release for each change. We'll also accumulate debt in the form of unused configuration properties unless we remember to remove them in future releases. Let’s look at a better way to change—and more importantly—manage feature toggles.
Getting Started With CloudBees Feature Management
First, before you can follow along, you’ll need to create a free CloudBees Feature Management account here. After you've finished registering, sign in to your CloudBees Feature Management account and create a new application.
Be sure to select iOS, as shown above. Next, you’ll see instructions for adding CloudBees Feature Management to your application.
First, download rox.zip, unzip it and add the rox directory to your project's directory tree. Next, add RoxSdk.xcodeproj and ROXCore.framework to your project. Then select Add Files to... from the project navigator.
Now select the files from the RoxSdk directory in the zip file output.
You'll see ROXCore.framework icon in the project navigator. Add those files. Next, we need to remove the framework from the project's linked frameworks. Select your project's name at the top of navigator tab.
Scroll down in the application information page to Linked Frameworks and Libraries.
Next, select ROXCore.framework and then click the minus to remove it. We want to add it back to embedded binaries. Click the plus in Embedded Binaries.
Then select ROXCore.framework and ROX.frameworkiOS and click Add. Last, we need to set up a build step to strip out unneeded libraries from our production application. First, add a script build phase.
Then locate the strip_frameworks.sh script.
And drag it into the script target.
Initialize the API
The Xcode project is ready now and we can initialize the API. Finally, click Setup SDK back at the CloudBees Feature Management dashboard.
The instructions in this dialog are for Swift. We'll need to make some adjustments for Objective-C. First, include the import statement for the RoxCore.h at the top of AppDelegate.m. Then add the call to ROX setupWithKey to didFinishLaunchingWithOptions. Pass it the unique initialization key CloudBees Feature Management assigned to our application. The top of AppDelegate.m should look similar to this:
#import "AppDelegate.h" #import <RoxCore/RoxCore.h> @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [ROX setupWithKey:@"5b1b19519373e2157eacf1e9"]; return YES; }
Now we can run the application to initialize CloudBees Feature Management. Click Activate App on the CloudBees Feature Management dashboard. After the application builds and runs, you are rewarded with this:
Add a Managed Feature Flag
Now we can add a managed feature toggle (Objective-C). If we look at our CloudBees Feature Management dashboard and check flags, we see this:
There are no flags and there is no way to add one. That's because we create them from our application, not from the dashboard. In Objective-C, feature flags are ROXFlags and are managed in RoxBaseContainers. First, add a header named MyContainer.h to the project.
#import <ROXCore/ROXCore.h> @interface MyContainer : ROXBaseContainer @property (nonatomic) ROXFlag* isNewYear; @end
Then add the corresponding implementation.
#import "MyContainer.h" @implementation MyContainer - (instancetype)init { self = [super init]; if (self) { self.isNewYear = [[ROXFlag alloc] init]; } return self; } @end
This gives us a single flag named isNewYear with a default value of false. It's managed in MyContainer, which we need to register with CloudBees Feature Management. We register the container with the API before calling ROX setupWithKey. Modify AppDelegate with the calls to create and register the container.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. self.myContainer = [[MyContainer alloc] init]; [ROX register:@"Flags" container: self.myContainer]; [ROX setupWithKey:@"5b1b19519373e2157eacf1e9"]; return YES; }
We pass two arguments in the message: a name for the container and an instance. Run the application and then go to the Flags menu item in the CloudBees Feature Management dashboard.
The new flag is there. Both the container name and the flag are on the dashboard.
Reading a Managed Feature Flag
Next, we'll update the application to use the RoxFlag instead of a property from a property list. ROX retrieves values asynchronously and caches them. On startup, it extracts the latest value from the cache and initiates a fetch from the network in parallel. It will also refresh the cache when the application regains focus. This means that the first time we run our application, we will immediately get the default value for our flag (false for toggles unless we override) and then the value set at CloudBees Feature Management upstream will be retrieved later. We want to see the value of the flag before we load our view, so our label is up-to-date when the application opens. We're going to create a configuration object for ROX and pass it to ROX setupWithKey.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. ROXOptions *options = [[ROXOptions alloc] init]; options.onConfigurationFetched = ^(ROXFetcherResult* result){ [[NSNotificationCenter defaultCenter] postNotificationName:@"LabelNotification" object:nil]; }; self.myContainer = [[MyContainer alloc] init]; [ROX register:@"Flags" container: self.myContainer]; [ROX setupWithKey:@"5b1b19519373e2157eacf1e9" options:options]; return YES; }
First, we create a RoxOptions. This holds configuration settings that we pass to setupWithkey. One of the possible options is a callback message that ROX sends when the latest configuration is fetched: onConfigurationFetched. It is passed a ROXFetcherResult that indicates whether or not the fetch was successful. In this example, we are assuming it was successful. When ROX sends the message, we use NSNotificationCenter to notify any interested objects that new information has been retrieved. There are a few advantages to using NotificationCenter instead of trying to call our view directly:
Since this callback will be called on startup, our ViewController probably hasn't been created yet.
If we add more controllers to the application, later things will start to get complicated.
Threading issues are delegated to listeners (as we'll see).
Next, we'll modify ViewController to process the update message and set the label.
- (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateLabels:) name:@"LabelNotification" object:nil]; } - (void) updateLabels:(NSNotification *) notification { dispatch_async(dispatch_get_main_queue(), ^{ AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; [appDelegate.myContainer.isNewYear enabled:^{ self->_greetingLabel.text = @"Happy Holidays!"; } disabled:^{ self->_greetingLabel.text = @"Hello, World!"; }]; }); }
ViewDidLoad is now subscribing to update messages and delegating them to a method named updateLabels. We moved control of our label to updateLabels, where we get a reference to AppDelegate to access its copy of MyContainer and use it. However, we have to dispatch this task to the main thread because only the UI thread can modify the view. Finally, isNewYear provides us with a block we use to set the label appropriately.
Managing a Feature Flag
We manage flags by adding them to experiments. An experiment is a set of criteria for controlling flags (and other variables) in production. First, click on Production in the left-hand side menu and then click Experiments. This will bring up a screen with a Create Experiment button. Click that and fill out the new experiment window appropriately.
Select Set Audience.
And we see a console for setting flags to true, false, or split. Leave the flag set to False for now. Run the application and you see "Hello, World!" Next, change the flag to True on the dashboard and restart the application.
We see the Happy New Year greeting! Flip it back to False and instead of stopping the app, click the phone's home button. Then tap on the application icon to open it again. The label now says Hello World! We can change the behavior of the application without touching code and releasing a new version. We can even alter behavior while the application is running on a client's device. Before we move on, let’s take a look at the experiment on the console again. Flip the flag from False to Split.
This option is where the term "experiment" comes from. We can distribute a flag's value to a percentage of users. It's a mechanism for implementing A/B testing in your application. This is the power of feature flags!
Extra Credit: Configurations
Tying the contents of a label to a true/false flag isn't what we would do in a production application but it kept the introduction to a boolean flag focused and simple. Let's take a look at a better mechanism for setting a label. CloudBees Feature Management supports managing configuration values from the dashboard. Let's add a new field to MyContainer. First, add a property to the MyContainer's header file.
@interface MyContainer : ROXBaseContainer @property (nonatomic) ROXFlag* isNewYear; @property (nonatomic) ROXConfigurationString *greetingText; @end
Then initialize it in the implementation file.
#import "MyContainer.h" @implementation MyContainer - (instancetype)init { self = [super init]; if (self) { self.isNewYear = [[ROXFlag alloc] init]; self.greetingText = [[ROXConfigurationString alloc] initWithDefaultValue:@"Hello, World!"]; } NSLog(@"Initializing container %p", self.isNewYear); return self; } @end
A RoxConfigurationString is precisely what it sounds like. It's a string that is controlled by Rox. Build and run the application again and then go back to the CloudBees Feature Management dashboard. Under Production and Configurations, we see our new value.
If we click on it, we can set it to a value like so:
Select >= for the criteria and enter "Happy New Year!" (without quotes). Then we need to make a small code change. Change updateLabels to use the value of our new property.
- (void) updateLabels:(NSNotification *) notification { dispatch_async(dispatch_get_main_queue(), ^{ AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; self->_greetingLabel.text = appDelegate.myContainer.greetingText.value; }); }
And run the app. We see Happy New Year! Let's change it.
Close and open the application. No need to restart or rebuild: our notification mechanism will handle the change.
We can change our application config with CloudBees Feature Management!
Conclusion
This guide demonstrates how to get started with CloudBees Feature Management with iOS and Objective-C. The API supports any iOS platform that runs Objective-C, including Apple TV. CloudBees Feature Management’s documentation has more information about how you can do a great deal more with flags, configuration settings, experiments, versions, and grouping users. We've written a simple application that demonstrates how feature flag management can help you manage your projects and eliminate unnecessary releases. Try it out on your own!