Architecting a Feature Toggle Service for iOS Apps

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.

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

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.

Swift

Start Your RxSwift Journey in Less Than 10 Minutes

RxSwift is well known for having a steep learning curve. But taking the time to learn it can easily be the next significant leap in your development abilities. We’ll cover the basic concepts of the library to quickly get you up to speed.