Go to file
Yakov Manshin 97533cbb51 Updated README
* Improved placement of code comments
* Removed mentions of old versions
* Various updates and improvements
2021-05-30 19:46:27 +03:00
.github/workflows [#69] Protocol-Based Resolver Init (#70) 2021-05-17 17:53:38 +03:00
Sources Updated FeatureFlagResolver.init(stores:) 2021-05-30 19:32:48 +03:00
Tests/YMFFTests [#68] Mutable Stores in Resolver Configuration (#72) 2021-05-20 22:15:20 +03:00
.gitignore Updated .gitignore 2020-12-25 22:58:40 +03:00
LICENSE [#51] Documentation Updates for v2.0 (#59) 2021-04-28 21:17:33 +03:00
Package.swift [#39] Protocols Target (#50) 2021-04-09 22:06:38 +03:00
README.md Updated README 2021-05-30 19:46:27 +03:00
YMFF.podspec Updated Podspec for v2.2.0 2021-05-20 22:16:52 +03:00

README.md

YMFF: Feature management made easy

Every company I worked for needed a way to manage availability of features in the apps already shipped to users. Surprisingly enough, feature flags (a.k.a. feature toggles a.k.a. feature switches) tend to cause a lot of struggle.

I aspire to change that.

YMFF is a nice little library that makes management of features with feature flags—and management of the feature flags themselves—a bliss, thanks to the power of Swifts property wrappers.

YMFF ships completely ready for use, right out of the box: you get everything you need to start in just a few minutes. But you can also replace nearly any component of the system with your own, customized implementation. The supplied implementation and the protocols are kept in two separate targets (YMFF and YMFFProtocols, respectively).

Installation

Swift Package Manager (SPM)

To add YMFF to your project, use Xcodes built-in support for Swift packages. Click File → Swift Packages → Add Package Dependency, and paste the following URL into the search field:

https://github.com/yakovmanshin/YMFF

Youre then prompted to select the version to install and indicate the desired update policy. I recommend starting with the latest version (its selected automatically), and choosing “up to next major” as the preferred update rule. Once you click Next, the package is fetched. Then select the target youre going to use YMFF in. Click Finish, and youre ready to go.

If you need to use YMFF in another Swift package, add it as a dependency:

.package(url: "https://github.com/yakovmanshin/YMFF", .upToNextMajor(from: "2.0.0"))

CocoaPods

YMFF now supports installation via CocoaPods.

Add the following to your Podfile:

pod 'YMFF', '~> 2.1'

Setup

All you need to start managing features with YMFF is at least one feature flag store—an object which conforms to FeatureFlagStoreProtocol and provides values that correspond to feature flag keys.

FeatureFlagStoreProtocol has two required methods: containsValue(forKey:) and value(forKey:).

Firebase Remote Config

Firebases Remote Config is one of the most popular tools to manage feature flags on the back-end side. Remote Configs RemoteConfigValue requires use of different methods to retrieve values of different types. Integration of YMFF with Remote Config, although doesnt look very pretty, is quite simple.

import FirebaseRemoteConfig
import YMFFProtocols

extension RemoteConfig: FeatureFlagStoreProtocol {
    
    public func containsValue(forKey key: String) -> Bool {
        self.allKeys(from: .remote).contains(key)
    }
    
    public func value<Value>(forKey key: String) -> Value? {
        // Remote Config returns a default value if the requested key doesnt exist,
        // so you need to check the key for existence explicitly.
        guard containsValue(forKey: key) else { return nil }
        
        let remoteConfigValue = self[key]
        
        // You need to use different `RemoteConfigValue` methods, depending on the return type.
        // I know, it doesnt look fancy.
        switch Value.self {
        case is Bool.Type:
            return remoteConfigValue.boolValue as? Value
        case is Data.Type:
            return remoteConfigValue.dataValue as? Value
        case is Double.Type:
            return remoteConfigValue.numberValue.doubleValue as? Value
        case is Int.Type:
            return remoteConfigValue.numberValue.intValue as? Value
        case is String.Type:
            return remoteConfigValue.stringValue as? Value
        default:
            return nil
        }
    }
    
}

Now RemoteConfig is a valid feature flag store.

Alternatively, you can create a custom wrapper object instead of extending RemoteConfig. Thats what I prefer to do in my projects for better flexibility.

Usage

Heres the most basic way to use YMFF:

import YMFF

// For convenience, organize feature flags in a separate namespace using an enum.
enum FeatureFlags {
    
    // `resolver` references one or more feature flag stores.
    private static var resolver = FeatureFlagResolver(configuration: .init(stores: [
        // If you want to change feature flag values from within your app, youll need at least one mutable store.
        .mutable(RuntimeOverridesStore()),
        // `MyFeatureFlagStore.shared` conforms to `FeatureFlagStoreProtocol`.
        .immutable(MyFeatureFlagStore.shared),
    ]))
    
    // Feature flags are initialized with three pieces of data:
    // a key string, the default value (used as fallback
    // when all feature flag stores fail to provide one), and the resolver.
    @FeatureFlag("promo_enabled", default: false, resolver: resolver)
    static var promoEnabled
    
    // Feature flags aren't limited to booleans. You can use any type of value.
    @FeatureFlag("number_of_banners", default: 3, resolver: resolver)
    static var numberOfBanners
    
}

To the code that makes use of a feature flag, the flag acts just like the type of its value:

if FeatureFlags.promoEnabled {
    displayPromoBanners(count: FeatureFlags.numberOfBanners)
}

Overriding Values

YMFF lets you override feature flag values in mutable stores from within your app. When you do, the new value is set to the first mutable store found in resolver configuration.

Overriding a feature flag value is as simple as assigning a new value to the flag.

FeatureFlags.promoEnabled = true

If you can set a value, you should also be able to remove it. And you can, indeed. Calling removeValueFromMutableStore() on FeatureFlags projected value (i.e. the FeatureFlag instance itself, as opposed to its wrapped value) removes the value from the first mutable feature flag store which contains one.

// Here `FeatureFlags.$promoEnabled` has the type `FeatureFlag<Bool>`, 
// while `FeatureFlags.promoEnabled` is of type `Bool`.
FeatureFlags.$promoEnabled.removeValueFromMutableStore()

UserDefaults

You can use UserDefaults to read and write feature flag values. Your changes will persist when the app is restarted.

import YMFF

private static var resolver = FeatureFlagResolver(configuration: .init(stores: [.mutable(UserDefaultsStore())]))

Thats it!

More

You can browse the source files to learn more about the options available to you. An autogenerated documentation is available at opensource.ym.dev.

Contributing

Contributions are welcome!

Have a look at issues to see the projects current needs. Dont hesitate to create new issues, especially if you intend to work on them yourself.

If youd like to discuss something else regarding YMFF (or not), contact me via email (the address is in the profile).

YMFF is available under the terms of the Apache License, version 2.0. See the LICENSE file for details.

© 20202021 Yakov Manshin