- 1.1 @Flag Annotation
- 1.2 @Flag Supported Datatypes
- 1.3
Codable
types and@Flag
- 1.4 Computed @Flag
- 1.5 Load a Feature Flag Collection in a
FlagLoader
- 1.6 Configure Key Evaluation for
FlagsLoader
's@Flag
- 1.7 Query a specific data provider
- 1.8 Set Flag defaultValue at runtime
- 1.9 Reset Flag values
- 1.10 Reset LocalProvider values
To create a feature flag you must create a FlagProtocol
conform object and use the @Flag
property wrapper annotation.
Here some examples of feature flags:
@Flag(name: "Awesome Property",
key: "my_awesome_prop",
default: 0,
excludedProviders: [FirebaseProvider.self],
description: "This is a description of the property"
)
var awesomeProperty: Int
As you can see the @Flag
annotation allows you to define the following properties (don't worry, excluding description
all of them are optionals):
name
: Identify the name of the property. If not set the property name is used automatically. This value is used by the Flags Browser.key
: The key which identify this property to the data providers. If not specified the value is automatically generated by the tree structure using the string transformation assigned to theFlagLoader
instance *(for example if this property is inside theMyGroup
the default full key will bemy_group.awesome_property
).default
: the default fallback value is key's value is not returned by any of the specifiedFlagsLoader
's data providers.excludedProviders
: you can also exclude some data providers of theFlagLoader
instance which load this property by adding their types here. For example if you add[FirebaseProvider.type]
, the Firebase Remote Config service is never used when asked for this ff's value.description
: This is the only required parameter: it shortly describe what the flag is for. This is used by Flag Browser for providing context for the flags you are enabling/disabling in the UI, but it also provides context for future developers.
Most of the time you will need just set the description and the key of the feature flag, like:
@Flag(key: "ios_app_rating_mode", description: "What kind of popup to show")
var ratingMode: String?
The following ratingMode
describe an optional String
feature flag. When loaded into an instance the loader itself ask the value ios_app_rating_mode
to any specified data provider.
If no value is found the default
option is returned (in this case, as optional, it just return nil
).
RealFlags allows you to define your own feature flags; it supports any primitive type both in wrapped and optional form:
Bool
Int
&UInt
(in 8, 16, 32 and 64 variants)String
Data
Date
URL
Dictionary
andArray
where value is any object conform toFlagProtocol
JSON
viaJSONData
custom type
When you need to support a custom datatype you just need to make it conform to the Codable
protocol and FlagProtocol
; serialization/deserialization operations are performed automatically by the library.
This is an example with CLLocationCoordinate2D
which by default is not conform to Codable
protocol:
extension CLLocationCoordinate2D: FlagProtocol, Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(longitude)
try container.encode(latitude)
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
let longitude = try container.decode(CLLocationDegrees.self)
let latitude = try container.decode(CLLocationDegrees.self)
self.init(latitude: latitude, longitude: longitude)
}
}
Once you made it conform you are able to just use easily:
public struct MapFlags: FlagCollectionProtocol {
@Flag(default: CLLocationCoordinate2D(latitude: 33, longitude: 33), description: "...")
var defaultCoordinates: CLLocationCoordinate2D
public init() { }
}
Moreover all Codable
ready object are automatically conforms to FlagProtocol
so you can virtually use any object type as feature flag.
While you can define virtually any kind of data type as feature flag using the @Flag
annotation you must keep in mind not all data providers may supports them.
Sometimes you may need to create a feature flag where the value is a combination of other flags or runtime values you need to evaluate dynamically.
This is the perfect example to use the computedValue
of the @Flag
annotation.
computedValue
allows you to define a callback function which is called before any other defined provider.
When the function return a non nil
value it will be the value of the flag (and no further checks are made on providers).
If you return a nil
value the library will perform a default check to defined providers and default value.
computedValue
should be short but you may encounter situations where the value must be a bit complex to evaluate. In this case **we strongly suggest defining a private static func
in your struct and refer it into the flag definition. See the code below.
The following example defines a Bool
property hasPublishButton
where the value is returned by checking the current language of the app:
public struct MiscFlags: FlagCollectionProtocol {
// MARK: - Flags
@Flag(default: false, computedValue: MiscFlags.computedPublishButton, description: "")
var hasPublishButton: Bool
// MARK: - Computed Properties Functions
public init() { }
private static func computedPublishButton() -> Bool? {
Language.main.code == "it"
}
}
@Flag
allows you to describe a feature flag property.
However you can consider it as description of the property structure. Value for a feature flag is obtained by an instance of a FlagLoader
.
FlagLoader
load a structure and a list of data providers.
let firebase = FirebaseRemoteProvider()
let local = LocalProvider(localURL: localFile)
let appFlags = FlagsLoader(AppFeatureFlags.self, providers: [local, firebase])
appFlags
load the AppFeatureFlag
ff collection and use [local, firebase]
as ordered data providers.
This mean that when you ask for a value inside, ie:
let currentValue = appFlags.ratingMode
RealFlags ask for value in the following order:
- to
local
, if no value is returned then ask to - to
firebase
if no value is returned then - fallback
default
value
FlagLoader
instance can be initialized by also configuring how keys are evaluated for each @Flag
of the loaded collection.
Each @Flag
annotated property uses the automatic key evaluation; the key used to query a data provider is extracted from the position of the property in the nested structure (if any) plus the name of the variable itself.
Consider the following structure:
struct Flags: FlagCollectionProtocol {
@FlagCollection(description: "...")
var nested: NestedCollection
@Flag(default: false, description: "...")
var flatBoolean: Bool
}
struct NestedCollection: FlagCollectionProtocol {
@Flag(default: false, description: "...")
var nestedBoolean: Bool
}
You have two properties:
flatBoolean
is in the root structurenestedBoolean
is inside theNestedCollection
By default the key which RealFlags uses to query for values to any set data provider are:
flat_boolean
forflatBoolean
propertynested/nested_boolean
fornestedBoolean
property
You can print them, once loaded using:
print("flatBoolean key: \(loader.$flatBoolean.keyPath.fullPath))") // flat_boolean
print("nestedBoolean key: \(loader.nested.$nestedBoolean.keyPath.fullPath))") // nested/nested_boolean
You can configure how keys are evaluated by passing a KeyConfiguration
when initializing the FlagsLoader
:
let config = KeyConfiguration(prefix: "myapp_", pathSeparator: ".", keyTransform: .snakeCase)
let loader = FlagsManager(providers: [...], keyConfiguration: config)
A KeyConfiguration
defines:
prefix
: the prefix to append at the start of each evaluated key (in this case it will bemyapp/flat_boolean
andmyapp/nested/nested_boolean
)pathSeparator
: separator to use for each path component; by default is/
but you can choose, for example.
.keyTransform
: how the property name/collection name must be transformed. You can choose betweennone
(no transformation,flatBoolean
property's key stillflatBoolean
),kebabCase
(it will beflatBoolean
) orsnakeCase
(it will beflat_boolean
).
If you don't want to use the automatic key evaluation you can set the fixed key
value of the @Flag
:
@Flag(key: "nestedAwesomeProp", default: false, description: "...")
var nestedBoolean: Bool
In this case the automatic key evaluation is disabled for this property and nestedAwesomeProp
is the value to query to any data provider.
Sometimes you may need to query a specific provider for a value.
Consider the previous property and suppose you want query just the Firebase
provider:
let valueInFirebase = appFlags.$ratingMode.flagValue(from: FirebaseRemoteProvider.self)
The flagValue()
function (accessible via the $
of the property wrapper) allows you to specify a particular data provider to query.
NOTE: If provider is not previously set into the
FlagsLoader
the fallback value is returned instead.
Sometimes you may want to alter the default value of a Flag
set via the annotation default
parameters.
This is true when, for example, you have different target of your product with different values for some flag and you would avoid creating duplicate files for each Flags Collection blue print.
In this case you can define your own FlagsCollection
s and use the setDefaultValue()
on each different flag to setup your own value.
Consider this example:
struct Flags: FlagCollectionProtocol {
@FlagCollection(default: 100, description: "...")
var flagA: Int
@Flag(default: false, description: "...")
var flagB: Bool
}
public func setupFlagsByTarget {
self.loader = FlagsLoader(Flags.self, provider: [...])
#if TARGET_A
// Target A only differ for a 200 default value for flagA
loader.$flagA.setDefault(200)
#endif
#if TARGET_B
// Target B only differ in flagB which is false by default
loader.$flagB.setDefault(false)
#endif
#if TARGET_C
// Target C has the same false for flagB but a different value for flagA
loader.$flagA.setDefault(50)
#endif
}
If you need to reset the custom value set for a flag inside each of its set provider you can use resetValue()
:
try? loader.$flagA.resetValue()
This remove any custom value set for this flag in each of its writable provider.
You can also reset an entire LocalProvider
instance optionally backed by a disk file.
Just call resetAllData()
on your instance and both in-memory and disk value (when set) will be removed from the provider.