Automated Feature Flagging Integration for iOS — JustTweak V6

Andrew Steven Grant
Just Eat Takeaway-tech
7 min readOct 12, 2021

How the iOS team at Just Eat Takeaway.com improved the JustTweak library to simplify the process of adding feature flags.

Photo by Dan Meyers on Unsplash

Let’s face it, we have come a long way in 3.5 billion years. Gone are the days of residing in hydrothermal pools, bubbling away in what was essentially one long day spa. Humans are good at a lot of things — compassion, kindness, creativity, imagination, storytelling, reasoning, innovation — the list goes on! I think we can all agree that one thing we are not good at is repetitive manual tasks. One such task we face on the JET iOS team is writing descriptive (and sometimes rather lengthy) string literals when adding feature flags for experiment or feature rollouts.

One of the challenges when working on a platform with a high volume of daily users is introducing new features without negatively impacting the UX or conversion rate — small and seemingly insignificant changes can sometimes have an unexpected negative impact. To provide our users with the best possible experience we run almost all user facing changes as experiments and/or feature rollouts to minimize the risk both for our users and the business function. As we support a number of different countries we also use our feature flagging framework as a way to specify different values on a per tenant basis. There is already a great blog post here which I recommend reading if you are not familiar with JustTweak, as I will not be going into all of the details in this article.

The Problem

Adding feature flags to a project is a manual process and prone to errors. I think it is safe to say the quickest way to do this involves duplicating an existing feature flag and modifying to the new requirements. The process does not require any deep or logical thinking, so it is easy to make mistakes after copying the boilerplate code. We had around 500 feature flags at the time of writing in the local Tweaks.json file — each of these represents a single configurable feature used within the application. Adding a feature flag to the project is achieved in the following steps:

  1. Add the feature and variable(s) to the Tweaks.json file
“ui_customization”: {
“display_red_view”: {
“Title”: “Display Red View”,
“Description”: “shows a red view in the main view controller”,
“Group”: “UI Customization”,
“Value”: false
}
}

2. Define constants for both feature and variable(s) to match keys in Tweaks.json

struct Features {
static let uiCustomization = “ui_customization”
}
struct Variables {
static let displayRedView = “display_red_view”
}

3. Implement property access returning the value from the Tweak manager passing in the constants to identify the feature

var canShowRedView: Bool {
get {
tweakManager.tweakWith(feature: Features.uiCustomization, variable: Variables.displayRedView)?.boolValue ?? false
}
set {
tweakManager.set(newValue, feature: Features.uiCustomization, variable: Variables.displayRedView)
}
}

This is a contrived example of a feature flag name, in reality feature flags are named using a standardised approach following the format <domain>_<feature>. As a rule of thumb we prefer longer descriptive names over smaller ambiguous definitions. If we look at an example of feature variable names currently on the platform, it is easier to see how a lapse in concentration could cause a mismatch between the .json key and the constant:

  1. serp_show_temporarily_offline_restaurants_enabled
  2. apiclient_restaurant_recommendations_path
  3. order_details_driver_reassigned_status_enable

Almost every developer that has been involved with a feature rollout has been burnt with a mismatch between the local provider and the constant (this actually happened the week before we integrated JustTweak V6 into the app).

The release process for iOS takes longer than web or backend as app submissions require external review from the App Store, on top of this the release team has a strict policy on making changes after the internal beta and will only allow fixes for a crash or critical issue. If a feature or experiment is incorrectly configured it would simply be disabled in the release or reverted in extreme cases. There is no substitute for a clear testing strategy and execution of a comprehensive test plan, but we know that in reality software development can be challenging and sometimes mistakes are made.

The 2nd and 3rd code snippets are where the majority of the issues occur. It is inevitable that we need to add the feature definition in the .json file, but removing the additional boilerplate code should help to prevent mismatch errors.

Solution

Remove the need to manually create the constants and property accessors and generate the code from the single feature definition in the Tweaks.json.

From JustTweak V6 onwards we now have an embedded macOS command line tool project within the workspace named “TweakAccessorGenerator”. This project is written completely in Swift and is responsible for the code generation executable aptly named the “TweakAccessorGenerator”. This script takes the following 3 arguments:

  1. -l (local tweaks file path)
  2. -o (output folder)
  3. -c (configuration folder)

We use swift-argument-parser to validate the input arguments and provide messaging if any input validation issues occur.

The Tweaks are loaded from the local input file (Tweaks.json) and converted into an array of strongly typed Tweak objects. We parse the json dictionary and have custom logic that determines the valueType for each tweak. Templates define the basic structure of the 2 generated files and string interpolation is used to insert the dynamically created content into the appropriate places. The string representations are then written to the -o output folder destination.

private func tweakComputedProperty(for tweak: Tweak) -> String {
let propertyName = tweak.propertyName ?? tweak.variable.camelCased()
let feature = “\(featuresConst).\(tweak.feature.camelCased())”
let variable = “\(variablesConst).\(tweak.variable.camelCased())”
let castProperty = try! self.castProperty(for: tweak.valueType)
let defaultValue = try! self.defaultValue(for: tweak.valueType)
return “””
var \(propertyName): \(tweak.valueType) {
get { tweakManager.tweakWith(feature: \(feature), variable: \(variable))?.\(castProperty) ?? \(defaultValue) }
set { tweakManager.set(newValue, feature: \(feature), variable: \(variable)) }
}
“””
}

When building the TweakAccessorGenerator-Release scheme, a Post-action run script copies the executable from the derived data folder into the JustTweak project Assets folder. We utilize the CocoaPods 1.4.0 script_phase attribute (docs) combined with the before_compile attribute to automatically integrate the script into the app target Build Phases upon pod installation. The executable file is then run as part of this automatically integrated Build Phase in the app, so that each time the app is built the files are automatically regenerated to reflect any changes to the Tweaks.json file.

A Note On Custom Types

JustTweak supports the following fundamental types Int, Double, String and Bool, however custom types are not supported in the .json definition. Our modular architecture is built in such a way that each high level module defines the features it requires in a protocol. As such we extend the GeneratedTweakAccessor to conform to each of these public module protocols to allow us to inject the features only exposing the features that are required. The majority of the extensions declare protocol conformance with an empty extension, although custom types have additional mapping which convert the raw types into the custom types defined in the Settings protocols.

protocol ExampleProtocol {
var customType: CustomType { get }
}
extension GeneratedTweakAccessor: ExampleProtocol { var customType: CustomType {
get {
CustomType(rawValue: customType_raw)
}
set {
customType_raw = newValue.rawValue
}
}
}

Limitations

A limitation worth mentioning when using JustTweak V6 is that it is no longer possible to leverage the custom TweakProperty property wrappers when using the code generator tool. It is the responsibility of the consumer to configure the TweakManager (using any of the three out-of-the-box/custom providers) and inject it into the GeneratedTweakAccessor class initializer. The problem arises when propagating this tweak manager into the init function of the TweakProperty wrappers. It is not possible to use the uninitialized (injected) tweak manager instance member within a property initializer as property initializers run before ‘self’ is available.

Whilst we are eager to incorporate new Swift language features when they are available, we decided it was not a fundamental requirement for this work. The intrinsic value provided by the code generator tool far outweighs that of the property wrappers, however the property wrappers are still available to be used for those who do not wish to use the code generator tool.

Conclusions

We have been using the new version of the library since March 2021 and the major update has been a real success! Since consuming JustTweak V6 in the main application we have not experienced any of the issues described earlier involving mismatched feature keys and thus have avoided delays to product-led features in the past quarter. The newfound simplicity to configure a feature has reduced development time and has been warmly received by the team.

If your team is currently looking for a straightforward and battle-tested solution for managing feature flags, I recommend you to take a look at how JustTweak and the new code generator tool may simplify your implementation.

Just Eat Takeaway.com is hiring. Apply today!

--

--