Feature toggling is essential for keeping your apps flexible in production. Sometimes you want to try ideas fast to see if they stick. Maybe you want to split test two ideas and see what performs the best. Whatever the case may be, the feature toggling service should be highly scalable and not be a bottleneck in your workflow. Here's how you create a feature toggling service with a clean iOS architecture.
Architecting a Feature Toggle Service for iOS Apps
Written by
Feature toggling, also known as feature flags, is where you wrap features in your app with toggle points, which are conditions that determine if some feature is enabled or disabled. This brings advantages such as experimenting with split tests, percentage rollouts of a feature, release toggles used in implementing continuous delivery as well as maintenance toggles for disabling certain features e.g. to decrease the load.
Overview of the system
Now, let's have a brief overview of what we're going to implement. For our feature toggling system, we're going to have a few goals:
- It should be easy to use feature togging from any view controller. You should only need one line of code to check for a feature toggle.
- It should support any underlying provider of feature toggles (fetched from a remote service, a file, etc.)
- It should be easy to add, remove & modify feature toggles and they should be statically typed.
- It should be scalable and applicable even for enterprise apps.
We're looking for achieving a highly scalable solution that makes it easy to add new feature toggles without the need to recompile the entire service. For this, I propose an architecture with modularization and the right level of abstractions. Take a look at the following diagram:
This architecture will get the job done. The view controllers in our app should only know about the feature togging service which acts as an abstraction to the underlying providers there may be. By using extensions in each of our feature modules (e.g. CategoryListViewController and ItemListViewController are kept in separate modules), we can easily add new features to the system that are statically typed.
Feature Toggle Service
We'll begin with the models. First off, our feature toggles will be based on this FeatureToggle
struct. It contains a feature and a value that determines if the feature is enabled.
public struct FeatureToggle { let feature: Feature let enabled: Bool } extension FeatureToggle: Decodable { enum CodingKeys: String, CodingKey { case feature case enabled } }
Here's the Feature
struct representing a feature in the app.
public struct Feature: RawRepresentable { private let name: String public init(rawValue: String) { self.name = rawValue } public var rawValue: String { return self.name } } extension Feature: Decodable { enum CodingKeys: String, CodingKey { case name } }
Now, onto the feature toggle service that the view controllers interact with:
public final class FeatureToggleService { private init() { } static let shared = FeatureToggleService() private var featureToggles: [FeatureToggle] = [] public func fetchFeatureToggles(mainProvider: FeatureToggleProvider, fallbackProvider: FeatureToggleProvider?, completion: @escaping ([FeatureToggle]) -> Void) { mainProvider.fetchFeatureToggles { [weak self] fetchedFeatureToggles in guard let self = self else { return } if fetchedFeatureToggles.count > 0 { self.featureToggles = fetchedFeatureToggles } else if let fallbackProvider = fallbackProvider { self.useFallbackFeatureToggles(fallbackProvider) } completion(self.featureToggles) } } public func isEnabled(_ feature: Feature) -> Bool { let feature = featureToggles.first(where: { $0.feature == feature }) return feature?.enabled ?? false } private func useFallbackFeatureToggles(_ fallbackProvider: FeatureToggleProvider) { fallbackProvider.fetchFeatureToggles { [weak self] featureToggles in if let self = self { self.featureToggles = featureToggles } } } }
The feature toggle service has a function isEnabled
to check if a feature is enabled. Initially, you should call the fetchFeatureToggles
function to fetch features from the feature toggle provider that you pass to it. Having a separate function for fetching feature toggles gives you more control over the loading strategy (fetch the features at the right time).
Feature Toggle Providers
You need to implement the feature toggle provider you want to go with. This could be your own backend service or a SAAS solution such as Firebase Remote Config for fetching remote feature toggles.
This is the protocol that all providers should conform to:
public typealias FeatureToggleCallback = ([FeatureToggle]) -> Void public protocol FeatureToggleProvider { func fetchFeatureToggles(_ completion: @escaping FeatureToggleCallback) }
This FirebaseRemoteConfigProvider
struct is the concrete implementation of the protocol based on Firebase Remote Config. Firebase Remote Config not only allows for toggling features remotely without the need of an app release but enables other benefits such as split testing or percentage rollouts of a feature out of the box. If you have Firebase Remote Config added to your project, you can add it as a feature toggling provider like this:
public struct FirebaseRemoteConfigProvider: FeatureToggleProvider { public func fetchFeatureToggles(_ completion: @escaping FeatureToggleCallback) { let remoteConfig = RemoteConfig.remoteConfig() let keys = remoteConfig.allKeys(from: .remote) let featureToggles = keys.map { FeatureToggle(feature: Feature(rawValue: $0), enabled: remoteConfig[$0].boolValue) } completion(featureToggles) } }
You also need fallback toggles, in case your remote feature toggling provider fails. That's why we'll implement this LocalFeatureToggleProvider
struct that provides local toggles in our project:
public struct LocalFeatureToggleProvider: FeatureToggleProvider { public func fetchFeatureToggles(_ completion: @escaping FeatureToggleCallback) { let configuration = LocalFeatureToggleProvider.loadConfiguration() ?? [] completion(configuration) } }
Extend the LocalFeatureToggleProvider
struct for dealing with fetching the values from a JSON file:
extension LocalFeatureToggleProvider { static let jsonContainerName: String = "featureToggles" static let configurationName: String = "FeatureToggles" static let configurationType: String = "json" static func loadConfiguration() -> [FeatureToggle]? { guard let configurationURL = bundledConfigurationURL(), let data = try? Data(contentsOf: configurationURL) else { return nil } return parseConfiguration(data: data) } static func parseConfiguration(data: Data) -> ParsingServiceResult? { return JSONParsingService().parse(data, containerName: jsonContainerName) } static func bundledConfigurationURL() -> URL? { return Bundle.main.url(forResource: configurationName, withExtension: configurationType) } }
This uses the following JSONParsingService
to parse the JSON to a FeatureToggle
array:
typealias ParsingServiceResult = [FeatureToggle] public protocol ParsingService { public func parse(_ data: Data, containerName: String) -> ParsingServiceResult? } public struct JSONParsingService: ParsingService { public func parse(_ data: Data, containerName: String) -> ParsingServiceResult? { var toggleData = data if let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments), let jsonContainer = json as? [String: Any], let featureToggles = jsonContainer[containerName], let featureTogglesData = try? JSONSerialization.data(withJSONObject: featureToggles) { toggleData = featureTogglesData } return try? JSONDecoder().decode([FeatureToggle].self, from: toggleData) } }
Finally, we can create a FeatureToggles.json
file in our project for each feature:
{ "featureToggles":[ { "feature":"addItem", "enabled":true }, { "feature":"addCategory", "enabled":true } ] }
That's it. Next, we'll look at how you use this in your project.
Usage
When you want to fetch your feature depends on your loading strategy. Perhaps you want to display a spinner until features are fetched, so features don't suddenly toggle itself on/off in front of the user when the fetchFeatureToggles
call returns.
Here's an example where feature toggles are simply fetched in SceneDelegate
:
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { ... scene setup ... prepareFirebaseRemoteConfig() let categoryVC = CategoryViewController(featureToggleService: .shared) window.rootViewController = UINavigationController(rootViewController: categoryVC) window.makeKeyAndVisible() } private func prepareFirebaseRemoteConfig() { FirebaseApp.configure() FeatureToggleService.shared.fetchFeatureToggles(mainProvider: FirebaseRemoteConfigProvider(), fallbackProvider: LocalFeatureToggleProvider()) { featureToggles in print("Feature toggles successfully fetched: ", featureToggles) } } }
Lastly, you can add your features from each feature module with an extension:
extension Feature { static let addCategory = Feature(rawValue: "addCategory") }
Now, we can use the feature toggle service to check if the feature should be enabled. In this example, we are wrapping the UIBarButtonItem
entry point to the add item feature in a todo app:
func setupNavigationBar() { if featureToggleService.isEnabled(.addCategory) { navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonPressed)) } }
Go ahead and try it out. You should be able to toggle the feature on/off and see the outcome in the app.
Conclusion
We looked at the benefits of feature toggling and how to use feature toggling for enabling all kinds of powerful capabilities. You can check out the source code that we covered here. Note that the sample code is a basic todo app without any modularization matching the diagram. That's an exercise for you when you implement this.
If you want to take a deep dive into feature toggling, I highly recommend that you read this article about Feature Toggles on Martin Fowler's blog.
Do you want to become an iOS architect? Check out iOS Architect Accelerator.
Share this post
Facebook
Twitter
LinkedIn
Reddit
You may also like
Process
How Do I Work?
Over the past few years, I’ve been exposed to various ideas and processes that have affected my way of working in software development teams. From my experiences, here’s what has helped me work as efficiently as possible as a software developer.
Design
How to Make Animated 3D Mockups in Seconds
As an individual app developer, you need to bring attention to your published apps. Companies of top-selling apps agree on how this is executed successfully. They create stunning and professional-looking animated 3D mockups of the apps. I’m here to tell you how you can create these for yourself in a matter of seconds.
DevOps
3 EAS Pipelines for Deploying React Native Apps in Teams
One of the best ways to increase productivity in a react native project is to automate the process of deploying builds you want to share internally in your team or with actual costumers via the app store. And you might be surprised at how easy it can actually be implemented when you have the right set of tools like GitHub Actions and EAS.