Today, we’re going to take a look at feature flagging in iOS. Though it’s a fairly intuitive idea, when done right, it opens the door to more robust functionality and improved app stability.

Feature flagging is simply a means of hiding and showing specific features in an application at runtime. With this approach, we can have shorter software integration cycles, easily implement A/B tests, and most importantly limit a user’s access to features in real-time.

Let’s take a look at how we can implement this in our projects.

Static Feature Flags

Most of the time when we’re developing code, we’ll have our master branch which we’ll try and keep deployable at all times. When we’re tasked with a new feature, we’ll branch off of master and continue all of our work on a specific feature branch.

Over time, this feature branch can get very large and it becomes difficult to maintain; you’ll have more merge conflicts, increased size of pull requests, limited ability to perform integration testing, different dependency versions, and so on.

With feature flags, we could allow this unfinished work to be merged into our main master branch, but prevent any user from seeing it. For example, we could use compiler directives to ensure that this new feature’s code isn’t even included in the compiled executable:

// Feature flags using compiler directives
#if SHOW_NEW_CHECKOUT 
	return CheckOutWithStripe()
#else
	return CheckOutWithPayPal()
#endif

Or, we could simply implement a feature flag as a boolean to selectively enable some feature:

if FeatureFlag.showNewCheckout.isEnabled {
  return CheckOutWithStripe()
} else {
	return CheckOutWithPayPal()
}

Here are a few ways of implementing static feature flags in our application:

// Using a struct
struct FeatureFlag {
	static let showNewCheckout = false
    static let limitCheckoutItems = 100
    static let showNewLoginScreen = true
}

// Using an enum
enum FeatureFlag {
    case showNewCheckout
    case enforceLimitCheckoutItems
    case showNewLoginScreen
    
    static var limitCheckoutItems = 100
    
    var enabled: Bool {
        switch self {
        case .showNewCheckout:
            return false
        case .enforceLimitCheckoutItems:
            return true
        case .showNewLoginScreen:
            return true
        }
    }
}

A feature flag doesn’t have to be limited to hiding or showing new features, we can use it to customize the behavior of existing ones.

If you intend to test variations on an existing feature, you’ll probably find the struct implementation the easiest. Generally speaking, I’ll only use the enum approach when I only intend to restrict access to new features.

There are a few other ways of setting up feature flags as well:

  • Loading flags from a local JSON file bundled with your project
  • Using .xcconfig which would allow you to enable different feature flags depending on the iOS target (i.e. production, staging, and development)

In just a moment, we’ll look at implementing a backend supported feature flagging system that will allow us to control access to features even after the application is live on the App Store.


Developer Menus

Every production application I’ve worked on has some variation of a “developer-only view” that allows you to simulate various states in your app. Sometimes it’s simple things like allowing you to easily switch between backend environments, resetting user defaults / cache, or more often allows you to control the current user’s settings and their account properties.

Feature flags lend themselves really well to enabling these types of “developer menus”. Surfacing your feature flags in these menus allows you and other internal testers to easily simulate and test all of the potential user flows in your application.


A/B Tests

Although we have only seen statically defined feature flags, we could easily pull our set of feature flags from an endpoint instead. As a result, feature flags could be repurposed for A/B testing.

If the clients fetched the current set of feature flags from the backend, we’d be able to easily specify whether the current user is part of the control or the experiment group and alter the user flow and analytics accordingly.

If our backend system was capable, we could also use this same approach to slowly roll out a new feature to a selected group of users. We could tweak the feature flag to only be true for 1% of users at first, then 5%, then 20%, and so on. With this approach, we could continuously look at our crash rate and our analytics and decide if we want to release this feature to more people. Otherwise, our only approach would be to release this new feature to everyone and hope that they like it and it’s stable.


Catching Crashes Early

No matter how many tests you write, sometimes a latent bug in your new feature will make it to production. If you have analytics and crash reporting integrated into your project, you’ll likely be able to catch this bug early.

And, if you used Apple’s phased roll out feature, you might even catch it before the new version of your app is available to the majority of your users.

But, what about your users that have already installed this buggy version?

Even if you release an expedited hotfix, it’ll still take some time for Apple to review it and all this while you are subjecting your user’s to a subpar experience.

Even when you choose to open up a new feature to all of your users, it can sometimes be prudent to wrap it in a feature flag as a precaution. This way, if you catch a trending stability issue, you will be able to toggle the feature flag and return your users to the previous flow (which is likely more tested) all without releasing a new app version.

Then, in a few weeks time, once you’re sure the feature is stable, you can simply remove the feature flag from your codebase and issue a new release. While the use of feature flags does marginally increase the amount of maintenance work required, in my opinion having the peace of mind of knowing that I can mitigate issues in production is well worth the extra effort.


Fetching User Specific Feature Flags

As previously mentioned, feature flags are not limited to being statically defined in our code. We can leverage our backend to generate a custom set of feature flags for every user.

Imagine that this is the response from your applications /api/feature_flag endpoint:

{
	"id": "aryamansharda",
	"featureFlags": [
		"expeditedBooking": true,
		"surgePricing": true,
		"referralProgram": false,
		"newOnboardingFlowControlGroup": false,
		"newCheckoutFlowControlGroup": true,
		...
	]
}

Assuming we passed in the id as part of the request, we now have user-specific feature flags we can use to control the experience in our app. In this user’s case, they have access to the expedited booking and surge pricing features, but they don’t have access to the referral program. Similarly, we can see that they are not part of the new onboarding flow experiment group, but are for the new checkout flow.

We might have the counterpart model on the iOS side look like this:

struct FeatureFlags: Codable {
	var expeditedBooking: Bool
	var surgePricing: Bool
	var referralProgram: Bool
	var newOnboardingFlowControlGroup: Bool
    var newCheckoutFlowControlGroup: Bool
}

A more robust implementation would include default values for these feature flags just to handle a case where the API call fails, the user is offline, etc. You may also want to save these feature flags to disk so when the user launches the app next time, their experience is still consistent.

We could change these values on the backend at any time and if our iOS app queries these flags periodically, we could unlock new flows and A/B tests all without releasing a new version.

Conclusion

Feature flags are one of the few commonalities across all iOS teams I’ve worked on. And yes, while they take a little bit of time to setup, I’ve found that in every project they’ve increased my confidence in releasing new features and greatly enhanced my testing abilities (via the developer menu).

As an added benefit, whenever I’m developing a new feature and I want to compare the new behavior against the original implementation, it’s so much faster to go into the FeatureFlag struct, flip the value, and run the app again.

Otherwise, I’d have to change branches, wait for Xcode to finish indexing, potentially resolve differences in dependency versions, and wait for the whole module to be recompiled before I could run the app again.

In summary, with feature flags:

  • Our master branch of our application is always deployable and we can minimize merge conflicts with feature branches
  • We can slowly release a new feature instead of all at once
  • We can test our application more thoroughly (via developer menu)
  • Further customize user experience
  • Implement A/B tests
  • Catch and mitigate issues in production without creating a new release

I hope you found this article useful. If you’d like to see more content like this consider checking out my YouTube channel about iOS and Swift Development.

I’ve also written a book on iOS/Swift tips I wish I knew when I was starting out – it’s free to download here.

Show CommentsClose Comments

1 Comment

  • Dave Jacobsen
    Posted July 29, 2021 at 11:33 pm 0Likes

    Killer article! I’ve actually never worked directly with feature flags so this was very informative.

Leave a comment