A feature flag is a simple concept, basically having a key/value pair where the key is a feature name and the value a boolean indicating if the feature is enabled or not.
It is a good practice to put your features behind a feature flag, especially on mobile where the software is distributed, not deployed.
What that means is, once you published your new build to the store, it takes at least a day or two to push another version, making it difficult to respond quickly to a problematic build.
Feature flags allow you to:
- Hide unfinished, untested or bugged features
- Rollout new features slowly until you are ready for a full release
- Test different features in A/B test fashion
I’ve used feature flags a lot simply to allow me to merge work-in-progress code to my project without having to maintain a feature branch with all the complications this comes with.
How Feature Flags Work
Typically, you will use a cloud-based feature flag provider to host a list of key/value pairs. These companies typically provide an SDK you can integrate that will ping their server to download a file locally, containing the list of flags and their respective values.
To over-simplify, this is what a feature flag file would look like:
{
"featureFlag1": true,
"featureFlag2": false,
…
}
A list of key/value pairs, the key being the feature flag name and the value a boolean indicating if the flag is active or not.
In your code, you can have conditional code that will look at the feature flag boolean value to determine if a feature is available or not.
if featureFlag1 == true {
// Navigate to the new version
} else {
// Navigate to the old version
}
Typical Implementation
In order to abstract the feature flag implementation, we can introduce a simple wrapper to access the flags. In this example, I create a dummy client representing a feature flag service SDK.
struct FeatureFlagService {
static let shared = FeatureFlagService()
private let client = OurFeatureFlagClient(key: "1234")
func isFlagActive(_ flag: String) -> Bool {
return client.flagValue(flag)
}
}
Most feature flag services will also ask for additional attributes, such as a user ID or session attributes, to allow user segmentation when rolling out a feature flag, but that’s not relevant for this article.
We can now use this service everywhere in our code.
class MyViewController: UIViewController {
private let featureFlagService = FeatureFlagService.shared
// …
func navigateToNextScreen() {
if featureFlagService.isFlagActive(myFlag) {
// Push View Controller A
} else {
// Push View Controller B
}
}
}
Debug Support
At the moment, if we want to test our feature flags, we must use the feature flag service wrapped in our struct. This makes it hard when you want to debug your code without impacting other people by turning the flag ON or OFF on demand. It also makes it hard to mock for your tests.
First step is to make a protocol definition of the feature flag store to allow for mocking.
protocol FeatureFlagServicing {
func isFlagActive(_ flag: String) -> Bool
}
class MyViewController: UIViewController {
var featureFlagService: FeatureFlagServicing = FeatureFlagService.shared
// …
}
You can now override the property featureFlagService
in your tests with a mock implementation.
Next, we can use a local feature flag store to allow switching values locally on demand. This store relies on UserDefaults
to store the key/value pairs that we need.
struct FeatureFlagStore {
private let userDefaults: UserDefaults
init(userDefaults: UserDefaults = UserDefaults.standard) {
self.userDefaults = userDefaults
}
static func defaultConfiguration() {
let defaultConfig = [
"featureFlag1": true,
"featureFlag2": true
]
let store = FeatureFlagStore()
defaultConfig.forEach { flag, value in
store.setFlag(flag, withValue: value)
}
}
func flagValue(_ flag: String) -> Bool {
return userDefaults.bool(forKey: flag.rawValue)
}
func setFlag(_ flag: String, withValue value: Bool) {
userDefaults.set(value, forKey: flag.rawValue)
userDefaults.synchronize()
}
}
Finally, we need to provide the option between local feature flags and live feature flags coming from the feature flag service.
enum FeatureFlagConfiguration {
case local, live
}
struct FeatureFlagService {
static let shared = FeatureFlagService()
var configuration: FeatureFlagConfiguration = .live
private let client = OurFeatureFlagClient(key: "1234")
private let store = FeatureFlagStore()
func isFlagActive(_ flag: String) -> Bool {
switch configuration {
case .local:
return store.getFlag(flag)
case .live:
return client.flagValue(flag)
}
}
}
By changing the feature flag service configuration, you will be able to switch between a local and a live configuration, and set flag values locally for your testing.
Using Property Wrappers
Swift 5.1 introduce property wrappers, a neat way to customize a property with business logic on how to get and/or set a property value. For more information on how property wrappers work, I recommend reading the NSHipster article about it.
We can leverage the property wrappers API to wrap our feature flags into one property definition.
struct Configuration {
private let userDefaults: UserDefaults
init(userDefaults: UserDefaults = UserDefaults.standard) {
self.userDefaults = userDefaults
}
var featureFlagMode: FeatureFlagConfiguration {
get {
guard let flagMode = userDefaults.object(forKey: DefaultsKey.featureFlagMode) as? String else {
return .live
}
return FeatureFlagMode(rawValue: flagMode) ?? .live
}
set {
userDefaults.setValue(newValue.rawValue, forKey: DefaultsKey.featureFlagMode)
userDefaults.synchronize()
}
}
}
@propertyWrapper struct Flag {
let name: String
var configuration = Configuration()
var flagService: FeatureFlagServicing = FeatureFlagService.shared
var flagStore = FeatureFlagStore()
var wrappedValue: Bool {
get {
switch configuration.featureFlagMode {
case .live:
return flagService.isFlagActive(name)
case .local:
return flagStore.flagValue(name)
}
}
set {
flagStore.setFlag(name, withValue: newValue)
}
}
}
With the property wrapper definition, we allow specifying the feature flag name, configuration, flag service and flag store, but we also provide default values for your regular setup.
You can then define your list of feature flags in a separate struct, specifying the flag name, configuration, store, etc.
protocol FeatureFlaggable {
var isFeatureFlag1Enabled: Bool
}
struct FeatureFlags: FeatureFlaggable {
@Flag(name: "featureFlag1")
var isFeatureFlag1Enabled: Bool
}
If you change the featureFlagMode
in Configuration
, all your Flag property wrappers will use the mode you selected automatically the next time you get their value.
The FeatureFlaggable
protocol gives us a way to mock the value of our feature flags in our tests.
The Flag
property wrapper gives us a very nice API to create new flags and configure them differently if needed. A lot of the implementation is hidden, making them very easy to use. The caveat is that discoverability of the feature can be a bit more difficult, as well as the documentation of it.