FeatureFlags makes it easy to configure feature flags, A/B and MVT tests via a JSON file which may be bundled with your app or hosted remotely. For remotely-hosted configuration files, you may enable / disable features without another release to the App Store, update the percentages of users in A/B test groups or even roll out a feature previously under A/B test to 100% of your users once you have decided that the feature is ready for prime time.
To learn more about how to use FeatureFlags, take a look at the keynote presentation, check out the blog post, or make use of the table of contents below:
Carthage is a dependency manager which produces a binary for manual integration into your project. It can be installed via Homebrew using the commands:
brew update
brew install carthage
In order to integrate FeatureFlags into your project via Carthage, add the following line to your project’s Cartfile:
github "rwbutler/FeatureFlags"
From the macOS Terminal run carthage update --platform iOS to build the framework then drag FeatureFlags.framework into your Xcode project.
The Swift Package Manager is a dependency manager for Swift modules and is included as part of the build system as of Swift 3.0. It is used to automate the download, compilation and linking of dependencies.
To include FeatureFlags as a dependency within a Swift package, add the package to the dependencies entry in your Package.swift file as follows:
With the framework integrated into your project, the next step is configuration using a JSON file which may be bundled as part of your app or hosted remotely. The JSON file may be newly-created or could be an existing configuration JSON file that you’re using already. Simply add a key called features at the top level of your file mapping to an array of features as follows:
{
"features": []
}
The contents of the array depends on the feature flags and tests to be configured.
To let FeatureFlags know where to find your configuration file:
Your remotely-hosted JSON file will always take precedence over bundled settings and remotely-defined settings will be cached so that in the eventuality that the user is offline, the last settings retrieved from the network will be applied.
Feature Flags
In order to configure a feature flag add a feature object to the features array in your JSON configuration.
Make sure that the raw value matches the string in your JSON file. Then call the following to check whether the feature flag is enabled:
Feature.isEnabled(.exampleFeatureFlag))
If the string specified in your Feature.Name extension doesn’t match the value in your JSON file, the default value returned is false. If you need to check the feature exists, you can write:
if let feature = Feature.named(.exampleFeatureFlag) {
print("Feature name -> \(feature.name)")
print("Feature enabled -> \(feature.isEnabled())")
}
A/B Tests
To configure an A/B test, add the following feature object to the features array in your JSON file:
enabled indicates whether or not the A/B test is enabled.
The only difference between a feature flag and an A/B test involves adding an array of test variations. FeatureFlags will assume that you are configuring an A/B test if you add two test variations to the array - add any more and the test will automatically become a multivariate test (MVT).
Import your feature into code with an extension on Feature.Name:
And then use the following to check which group the user has been assigned to:
if let test = ABTest(rawValue: .exampleABTest) {
print("Is in group A? -> \(test.isGroupA())")
print("Is in group B? -> \(test.isGroupB())")
}
Alternatively, you may prefer the following syntax:
if let feature = Feature.named(.exampleABTest) {
print("Feature name -> \(feature.name)")
print("Is group A? -> \(feature.isTestVariation(.groupA))")
print("Is group B? -> \(feature.isTestVariation(.groupB))")
print("Test variation -> \(feature.testVariation())")
}
Feature A/B Tests
A feature A/B test is a subtle variation on (and subtype of) an A/B test. In a generic A/B test you may want to check whether a user has been placed in the blue background or red background test variation. A feature A/B test specifically tests whether the introduction of a new feature is an improvement over a control group without the feature. Thus in a feature A/B test - the feature is either off or on.
To configure a feature A/B test use the following JSON:
By naming the test variations Enabled and Disabled, FeatureFlags knows that your intention is to set up a feature A/B test.
Configuring a feature A/B test has the advantage over a generic A/B test in that instead of having to write:
if let feature = Feature.named(.exampleFeatureABTest) {
print("Feature name -> \(feature.name)")
print("Is group A? -> \(feature.isTestVariation(.enabled))")
print("Is group B? -> \(feature.isTestVariation(.disabled))")
print("Test variation -> \(feature.testVariation())")
}
You may simply use the following to determine which test group the user has been assigned to:
Feature.isEnabled(.exampleFeatureABTest))
Ordinarily using the Feature.enabled() method tests to see whether a feature is globally enabled; in this specific instance it will return true if the user belongs to the group receiving the new feature and false if the user belongs to the control group. Note that this method also return false if the enabled property is set to false in the JSON for this feature i.e. the test is globally disabled.
Multivariate (MVT) Tests
Configuration of a multivariate test follows much the same pattern as that of an A/B test. Add the following feature object to the features array in your JSON file:
enabled indicates whether or not the MVT test is enabled.
FeatureFlags knows that you are configuring a MVT test if you add more than two test variations to the array. Again, import your feature into code with an extension on Feature.Name:
Using the following to check which group the user has been assigned to:
if let feature = Feature.named(.exampleMVTTest) {
print("Feature name -> \(feature.name)")
print("Is group A? -> \(feature.isTestVariation(.groupA))")
print("Is group B? -> \(feature.isTestVariation(.groupB))”)
print("Is group C? -> \(feature.isTestVariation(.groupC))”)
print("Test variation -> \(feature.testVariation())”)
}
You are free to name your test variations whatever you wish:
enabled indicates whether or not the MVT test is enabled.
Simply create an extension on Test.Variation to map your test variations in code:
extension Test.Variation {
static let red = Test.Variation(rawValue: "Red")
static let green = Test.Variation(rawValue: "Green")
static let blue = Test.Variation(rawValue: "Blue")
}
Then check which group the user has been assigned to:
if let feature = Feature.named(.exampleMVTTest) {
print("Feature name -> \(feature.name)")
print("Is red? -> \(feature.isTestVariation(.red))")
print("Is green? -> \(feature.isTestVariation(.green))")
print("Is blue? -> \(feature.isTestVariation(.blue))")
print("Test variation -> \(feature.testVariation())")
}
Development Flags
When developers speak of feature flags they are often referring to one of two things:
Remote flags: Allow us to remotely toggle a finished feature on or off and roll it out to a specific group of users.
Development flags: Allow to us hide features in development in order to keep code shippable.
With development flags we never want the code under development to be released to users.
Consider if we were to use a remote feature flag to toggle off an unfinished feature allowing us to release version 1 of our app without the feature present. If subsequently we were to finish the feature as part of version 2 of the app and toggle the feature on then users of version one would experience a partially complete feature as the under development version 1 code is enabled. This is a situation we never want to arise hence FeatureFlags caters for development flags as well as remote flags.
To mark a feature flag as a development flag, first of all include a bundled JSON file as part of your app containing the features key. The JSON file may be an existing file or an entirely new file. Next, having defined your feature flags as part of this file, set the configuration URL to reference this file:
And that’s it! From now on this feature flag will be considered a development flag and the code behind it will never be released to users even if enabled is set.
Development flag code will be shown in the following cases:
If the #DEBUG preprocessor flag is set and the flag is enabled.
If FeatureFlags.isDevelopment is set to true (it is up to the developer to set this to true when the app is in development - the default value is false) and the flag is enabled.
If you need more granular control over code that is in development then you may pass the isDevelopment flag when checking whether or not a feature is enabled e.g.:
if let feature = Feature.named(.exampleFeatureFlag, isDevelopment: true) {
print("Feature name -> \(feature.name)")
print("Feature enabled -> \(feature.isEnabled())")
}
In this example, the print statements will only be executed if exampleFeatureFlag is enabled and the app is development (i.e. either #DEBUG or FeatureFlags.isDevelopment is set).
Unlock Flags
Regardless of whether a feature flag is controlled locally or remotely, the types of flag above operate at global level i.e. they will enable or disable a feature for all users. But if we want to unlock a feature for individual users following an in-app purchase or as a reward for the completion of some goal then we need a way to unlock the feature and have it remain unlocked - we achieve this with unlock flags.
To configure an unlock flag in your configuration JSON file add the following:
The unlocked property indicates to FeatureFlags that this is to be an unlock flag whose default value is false i.e. the feature will be locked to begin with. Note that if enabled property is set to false then the feature will be disabled for all users regardless of whether the feature has been unlocked for a particular user as this property operates at a global level.
It is possible to query whether or not an unlock flag is unlocked as follows:
if let feature = Feature.named(.exampleUnlockFlag) {
print("Is unlocked? -> \(feature.isUnlocked())")
}
When you wish to unlock a feature for the user, call feature.unlock(). Conversely, if you only wish to unlock a feature for a certain period of time e.g. to allow the user to trial a feature, you may later call feature.lock() to make the feature unavailable again. Both methods return a Bool to indicate whether or not the feature has been unlocked / locked.
if let feature = Feature.named(.exampleUnlockFlag) {
print("Is unlocked? -> \(feature.unlock())")
}
Note that a feature may not be unlocked if:
The feature flag is not an unlock flag i.e. the unlocked property was not defined in JSON.
The enabled property is set to false.
Advanced Usage
Test Bias
By default for any A/B or MVT test, the user is equally likely to be assigned each of the specified test variations i.e. for an A/B test, there’s a 50%/50% chance of being assigned one group or another. For a MVT test with four variations, the chance of being assigned to each is 25%.
It is possible to configure a test bias such that the likelihood of being assigned to each test variation is not equal. To do so, simply add the following JSON to your feature object:
The number of weightings specified in the test-biases array must be equal to the number of test variations and must amount to 100 otherwise the weightings will be ignored and default to equal weightings.
Labels
It is possible to attach labels to test variations in case you wish to send analytics respective to the test group to which a user has been assigned.
To do so, define an array of labels of equal length to the number of test variations specified:
Then to retrieve your labels in code you would write the following:
if let feature = Feature.named(.exampleABTest) {
print("Group A label -> \(feature.label(.groupA))")
print("Group B label -> \(feature.label(.groupB))")
}
Rolling Out Features
The most powerful feature of the FeatureFlags framework is the ability to adjust the test biases in your remote JSON configuration file and have the users automatically be re-assigned to new test groups. For example, you might decide to roll out a feature using a 10%/90% (whereby 10% of users receive the new feature) split in the first week, 20%/80% in the second week and so on.
Simply update the weightings in the test-biases array and the next time the framework checks your JSON configuration, groups will be re-assigned.
When you are done A/B or MVT testing a feature you will have gathered enough analytics to decide whether or not to roll out the feature to your entire user base. At this point, you may decide to disable the feature entirely by setting the enabled flag to false in your JSON file or in the case of a successful test, you may decide to roll out the feature to all users by adjusting the feature object in your JSON file from:
In order to test that both variations of your new feature work correctly you may need to adjust the status of your feature flags / tests at runtime. To this end FeatureFlags provides the FeatureFlagsViewController which allows you to toggle features flags on/off in debug builds of your app or cycle A/B testing or MVT testing variations.
To display the view controller specify the navigational preferences desired and then push the view controller by providing a UINavigationController:
Should you need further information on the state of each feature flag / test, you may use 3D Touch to peek / pop more information.
Refreshing Configuration
Should you need to refresh your configuration at any time you may call FeatureFlags.refresh() which optionally accepts a completion closure to notify you when the refresh is complete.
If you have opted to include your feature flag information as part of an existing JSON file which your app has already fetched you may wish to use the following method passing the JSON file data to avoid repeated network calls:
FeatureFlags.refreshWithData(_:completion:)
Objective-C
Whilst FeatureFlags is primarily intended for use by Swift apps, should the need arise to check whether a feature flag is enabled in Objective-C it is possible to do so as follows:
static NSString *const kMyNewFeatureFlag = @"My New Feature Flag";
if (FEATURE_IS_ENABLED(kMyNewFeatureFlag)) {
...
}
Connectivity - Improves on Reachability for determining Internet connectivity in your iOS application.
FeatureFlags - Allows developers to configure feature flags, run multiple A/B or MVT tests using a bundled / remotely-hosted JSON configuration file.
FlexibleRowHeightGridLayout - A UICollectionView grid layout designed to support Dynamic Type by allowing the height of each row to size to fit content.
Hash - Lightweight means of generating message digests and HMACs using popular hash functions including MD5, SHA-1, SHA-256.
Skylark - Fully Swift BDD testing framework for writing Cucumber scenarios using Gherkin syntax.
TailorSwift - A collection of useful Swift Core Library / Foundation framework extensions.
TypographyKit - Consistent & accessible visual styling on iOS with Dynamic Type support.
Updates - Automatically detects app updates and gently prompts users to update.
FeatureFlags makes it easy to configure feature flags, A/B and MVT tests via a JSON file which may be bundled with your app or hosted remotely. For remotely-hosted configuration files, you may enable / disable features without another release to the App Store, update the percentages of users in A/B test groups or even roll out a feature previously under A/B test to 100% of your users once you have decided that the feature is ready for prime time.
To learn more about how to use FeatureFlags, take a look at the keynote presentation, check out the blog post, or make use of the table of contents below:
Features
What’s new in FeatureFlags 3.0.0?
See CHANGELOG.md.
Installation
Cocoapods
CocoaPods is a dependency manager which integrates dependencies into your Xcode workspace. To install it using RubyGems run:
To install FeatureFlags using Cocoapods, simply add the following line to your Podfile:
Then run the command:
For more information see here.
Carthage
Carthage is a dependency manager which produces a binary for manual integration into your project. It can be installed via Homebrew using the commands:
In order to integrate FeatureFlags into your project via Carthage, add the following line to your project’s Cartfile:
From the macOS Terminal run
carthage update --platform iOS
to build the framework then dragFeatureFlags.framework
into your Xcode project.For more information see here.
Swift Package Manager
The Swift Package Manager is a dependency manager for Swift modules and is included as part of the build system as of Swift 3.0. It is used to automate the download, compilation and linking of dependencies.
To include FeatureFlags as a dependency within a Swift package, add the package to the
dependencies
entry in yourPackage.swift
file as follows:Usage
With the framework integrated into your project, the next step is configuration using a JSON file which may be bundled as part of your app or hosted remotely. The JSON file may be newly-created or could be an existing configuration JSON file that you’re using already. Simply add a key called
features
at the top level of your file mapping to an array of features as follows:The contents of the array depends on the feature flags and tests to be configured.
To let FeatureFlags know where to find your configuration file:
Or:
In the event that you opt to host your JSON file remotely, you may provide a bundled fallback as part of your app bundle:
Your remotely-hosted JSON file will always take precedence over bundled settings and remotely-defined settings will be cached so that in the eventuality that the user is offline, the last settings retrieved from the network will be applied.
Feature Flags
In order to configure a feature flag add a feature object to the features array in your JSON configuration.
Then add an extension on
Feature.Name
to import your feature flag in code as follows:Make sure that the raw value matches the string in your JSON file. Then call the following to check whether the feature flag is enabled:
If the string specified in your
Feature.Name
extension doesn’t match the value in your JSON file, the default value returned isfalse
. If you need to check the feature exists, you can write:A/B Tests
To configure an A/B test, add the following feature object to the features array in your JSON file:
enabled
indicates whether or not the A/B test is enabled.The only difference between a feature flag and an A/B test involves adding an array of test variations. FeatureFlags will assume that you are configuring an A/B test if you add two test variations to the array - add any more and the test will automatically become a multivariate test (MVT).
Import your feature into code with an extension on
Feature.Name
:And then use the following to check which group the user has been assigned to:
Alternatively, you may prefer the following syntax:
Feature A/B Tests
A feature A/B test is a subtle variation on (and subtype of) an A/B test. In a generic A/B test you may want to check whether a user has been placed in the blue background or red background test variation. A feature A/B test specifically tests whether the introduction of a new feature is an improvement over a control group without the feature. Thus in a feature A/B test - the feature is either off or on.
To configure a feature A/B test use the following JSON:
enabled
indicates whether or not the A/B test is enabled.By naming the test variations
Enabled
andDisabled
, FeatureFlags knows that your intention is to set up a feature A/B test.Configuring a feature A/B test has the advantage over a generic A/B test in that instead of having to write:
You may simply use the following to determine which test group the user has been assigned to:
Ordinarily using the
Feature.enabled()
method tests to see whether a feature is globally enabled; in this specific instance it will returntrue
if the user belongs to the group receiving the new feature andfalse
if the user belongs to the control group. Note that this method also returnfalse
if theenabled
property is set tofalse
in the JSON for this feature i.e. the test is globally disabled.Multivariate (MVT) Tests
Configuration of a multivariate test follows much the same pattern as that of an A/B test. Add the following feature object to the features array in your JSON file:
enabled
indicates whether or not the MVT test is enabled.FeatureFlags knows that you are configuring a MVT test if you add more than two test variations to the array. Again, import your feature into code with an extension on
Feature.Name
:Using the following to check which group the user has been assigned to:
You are free to name your test variations whatever you wish:
enabled
indicates whether or not the MVT test is enabled.Simply create an extension on
Test.Variation
to map your test variations in code:Then check which group the user has been assigned to:
Development Flags
When developers speak of feature flags they are often referring to one of two things:
With development flags we never want the code under development to be released to users.
Consider if we were to use a remote feature flag to toggle off an unfinished feature allowing us to release version 1 of our app without the feature present. If subsequently we were to finish the feature as part of version 2 of the app and toggle the feature on then users of version one would experience a partially complete feature as the under development version 1 code is enabled. This is a situation we never want to arise hence FeatureFlags caters for development flags as well as remote flags.
To mark a feature flag as a development flag, first of all include a bundled JSON file as part of your app containing the
features
key. The JSON file may be an existing file or an entirely new file. Next, having defined your feature flags as part of this file, set the configuration URL to reference this file:Or, if you are already using a remote configuration URL then set the fallback configuration URL instead:
Set up your feature flag in JSON as you would do normally but setting the
development
property totrue
:And that’s it! From now on this feature flag will be considered a development flag and the code behind it will never be released to users even if
enabled
is set.Development flag code will be shown in the following cases:
#DEBUG
preprocessor flag is set and the flag isenabled
.FeatureFlags.isDevelopment
is set totrue
(it is up to the developer to set this totrue
when the app is in development - the default value isfalse
) and the flag isenabled
.If you need more granular control over code that is in development then you may pass the
isDevelopment
flag when checking whether or not a feature is enabled e.g.:In this example, the
print
statements will only be executed ifexampleFeatureFlag
is enabled and the app is development (i.e. either#DEBUG
orFeatureFlags.isDevelopment
is set).Unlock Flags
Regardless of whether a feature flag is controlled locally or remotely, the types of flag above operate at global level i.e. they will enable or disable a feature for all users. But if we want to unlock a feature for individual users following an in-app purchase or as a reward for the completion of some goal then we need a way to unlock the feature and have it remain unlocked - we achieve this with unlock flags.
To configure an unlock flag in your configuration JSON file add the following:
The
unlocked
property indicates to FeatureFlags that this is to be an unlock flag whose default value isfalse
i.e. the feature will be locked to begin with. Note that ifenabled
property is set tofalse
then the feature will be disabled for all users regardless of whether the feature has been unlocked for a particular user as this property operates at a global level.It is possible to query whether or not an unlock flag is unlocked as follows:
When you wish to unlock a feature for the user, call
feature.unlock()
. Conversely, if you only wish to unlock a feature for a certain period of time e.g. to allow the user to trial a feature, you may later callfeature.lock()
to make the feature unavailable again. Both methods return aBool
to indicate whether or not the feature has been unlocked / locked.Note that a feature may not be unlocked if:
unlocked
property was not defined in JSON.enabled
property is set tofalse
.Advanced Usage
Test Bias
By default for any A/B or MVT test, the user is equally likely to be assigned each of the specified test variations i.e. for an A/B test, there’s a 50%/50% chance of being assigned one group or another. For a MVT test with four variations, the chance of being assigned to each is 25%.
It is possible to configure a test bias such that the likelihood of being assigned to each test variation is not equal. To do so, simply add the following JSON to your feature object:
The number of weightings specified in the
test-biases
array must be equal to the number of test variations and must amount to 100 otherwise the weightings will be ignored and default to equal weightings.Labels
It is possible to attach labels to test variations in case you wish to send analytics respective to the test group to which a user has been assigned.
To do so, define an array of
labels
of equal length to the number of test variations specified:Then to retrieve your labels in code you would write the following:
Rolling Out Features
The most powerful feature of the FeatureFlags framework is the ability to adjust the test biases in your remote JSON configuration file and have the users automatically be re-assigned to new test groups. For example, you might decide to roll out a feature using a 10%/90% (whereby 10% of users receive the new feature) split in the first week, 20%/80% in the second week and so on.
Simply update the weightings in the
test-biases
array and the next time the framework checks your JSON configuration, groups will be re-assigned.When you are done A/B or MVT testing a feature you will have gathered enough analytics to decide whether or not to roll out the feature to your entire user base. At this point, you may decide to disable the feature entirely by setting the
enabled
flag tofalse
in your JSON file or in the case of a successful test, you may decide to roll out the feature to all users by adjusting the feature object in your JSON file from:To a feature flag globally enabled for all users as follows:
QA
In order to test that both variations of your new feature work correctly you may need to adjust the status of your feature flags / tests at runtime. To this end FeatureFlags provides the
FeatureFlagsViewController
which allows you to toggle features flags on/off in debug builds of your app or cycle A/B testing or MVT testing variations.To display the view controller specify the navigational preferences desired and then push the view controller by providing a
UINavigationController
:Should you need further information on the state of each feature flag / test, you may use 3D Touch to peek / pop more information.
Refreshing Configuration
Should you need to refresh your configuration at any time you may call
FeatureFlags.refresh()
which optionally accepts a completion closure to notify you when the refresh is complete.If you have opted to include your feature flag information as part of an existing JSON file which your app has already fetched you may wish to use the following method passing the JSON file data to avoid repeated network calls:
Objective-C
Whilst FeatureFlags is primarily intended for use by Swift apps, should the need arise to check whether a feature flag is enabled in Objective-C it is possible to do so as follows:
Author
Ross Butler
License
FeatureFlags is available under the MIT license. See the LICENSE file for more info.
Additional Software
Controls
Frameworks
Tools
cdd
from the Terminal.