FeatureFlags

Build Status Version Carthage compatible Maintainability License Reviewed by Hound

FeatureFlags 使配置功能开关、A/B 测试和 MVT 测试变得容易,它通过 JSON 文件进行配置,该文件可以与您的应用程序捆绑在一起,也可以远程托管。对于远程托管的配置文件,您无需再次发布到 App Store 即可启用/禁用功能,更新 A/B 测试组中用户的百分比,甚至在您确定该功能已准备好投入生产环境后,将先前处于 A/B 测试中的功能推广到 100% 的用户。

要了解更多关于如何使用 FeatureFlags 的信息,请查看主题演讲稿,查看博客文章,或使用下面的目录

功能

FeatureFlags 3.0.0 中的新功能?

请参阅 CHANGELOG.md

安装

Cocoapods

CocoaPods 是一个依赖管理工具,它将依赖项集成到您的 Xcode 工作区中。要使用 RubyGems 安装它,请运行

gem install cocoapods

要使用 Cocoapods 安装 FeatureFlags,只需将以下行添加到您的 Podfile

pod "FeatureFlags"

然后运行命令

pod install

有关更多信息,请点击此处查看

Carthage

Carthage 是一个依赖管理工具,它生成一个二进制文件,用于手动集成到您的项目中。它可以通过 Homebrew 使用以下命令安装

brew update
brew install carthage

为了通过 Carthage 将 FeatureFlags 集成到您的项目中,请将以下行添加到您项目的 Cartfile 中

github "rwbutler/FeatureFlags"

从 macOS 终端运行 carthage update --platform iOS 来构建框架,然后将 FeatureFlags.framework 拖到您的 Xcode 项目中。

有关更多信息,请点击此处查看

Swift Package Manager

Swift Package Manager 是 Swift 模块的依赖管理工具,自 Swift 3.0 起已作为构建系统的一部分包含在内。它用于自动化依赖项的下载、编译和链接。

要将 FeatureFlags 作为依赖项包含在 Swift 包中,请将该包添加到您的 Package.swift 文件中 dependencies 条目中,如下所示

dependencies: [
    .package(url: "https://github.com/rwbutler/FeatureFlags.git", from: "2.0.0")
]

用法

将框架集成到您的项目后,下一步是使用 JSON 文件进行配置,该文件可以与您的应用程序捆绑在一起,也可以远程托管。JSON 文件可以是新创建的,也可以是您已在使用的现有配置 JSON 文件。只需在文件的顶层添加一个名为 features 的键,映射到一个功能数组,如下所示

{
    "features": []
}

数组的内容取决于要配置的功能开关和测试。

要让 FeatureFlags 知道在哪里找到您的配置文件

guard let featuresURL = Bundle.main.url(forResource: "features", withExtension: "json") else { return }
FeatureFlags.configurationURL = featuresURL

guard let featuresURL = URL(string: "https://www.exampledomain.com/features.json") else { return }
FeatureFlags.configurationURL = featuresURL

如果您选择远程托管您的 JSON 文件,您可以提供一个捆绑的后备方案作为应用程序包的一部分

guard let fallbackURL = Bundle.main.url(forResource: "features", withExtension: "json") else { return }
FeatureFlags.localFallbackConfigurationURL = fallbackURL

您的远程托管 JSON 文件将始终优先于捆绑设置,并且远程定义的设置将被缓存,以便在用户离线的情况下,将应用从网络检索到的最后设置。

功能开关

为了配置功能开关,请在 JSON 配置的功能数组中添加一个功能对象。

{
    "features": [{
        "name": "Example Feature Flag",
        "enabled": false
    }]
}

然后在 Feature.Name 上添加一个扩展,以便在代码中导入您的功能开关,如下所示

import FeatureFlags

extension Feature.Name {
	static let exampleFeatureFlag = Feature.Name(rawValue: "Example Feature Flag")
}

确保原始值与 JSON 文件中的字符串匹配。然后调用以下代码来检查功能开关是否已启用

Feature.isEnabled(.exampleFeatureFlag)) 

如果您的 Feature.Name 扩展中指定的字符串与 JSON 文件中的值不匹配,则返回的默认值为 false。如果您需要检查功能是否存在,您可以编写

if let feature = Feature.named(.exampleFeatureFlag) {
	print("Feature name -> \(feature.name)")
	print("Feature enabled -> \(feature.isEnabled())")
}

A/B 测试

要配置 A/B 测试,请将以下功能对象添加到 JSON 文件中的功能数组中

{
	"name": "Example A/B Test",
	"enabled": true,
	"test-variations": ["Group A", "Group B"]
}

功能开关和 A/B 测试之间的唯一区别在于添加了一个测试变体数组。如果您向数组添加两个测试变体,FeatureFlags 将假定您正在配置 A/B 测试 - 添加更多变体,测试将自动变为多变量测试 (MVT)。

使用 Feature.Name 上的扩展将您的功能导入代码

extension Feature.Name {
	static let exampleABTest = Feature.Name(rawValue: "Example A/B Test")
}

然后使用以下代码来检查用户被分配到哪个组

if let test = ABTest(rawValue: .exampleABTest) {
	print("Is in group A? -> \(test.isGroupA())")
	print("Is in group B? -> \(test.isGroupB())")
}

或者,您可能更喜欢以下语法

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())")
}

功能 A/B 测试

功能 A/B 测试是 A/B 测试的一种细微变体(和子类型)。在通用 A/B 测试中,您可能想检查用户是否被分配到蓝色背景或红色背景测试变体。功能 A/B 测试专门测试新功能的引入是否比没有该功能的对照组有所改进。因此,在功能 A/B 测试中 - 功能要么关闭,要么开启。

要配置功能 A/B 测试,请使用以下 JSON

{
	"name": "Example Feature A/B Test",
	"enabled": true,
	"test-variations": ["Enabled", "Disabled"]
}
extension Feature.Name {
	static let exampleFeatureABTest = Feature.Name(rawValue: "Example Feature A/B Test")
}

通过将测试变体命名为 EnabledDisabled,FeatureFlags 知道您的意图是设置功能 A/B 测试。

配置功能 A/B 测试优于通用 A/B 测试的优点在于,您不必编写

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())")
}

您可以简单地使用以下代码来确定用户已被分配到哪个测试组

Feature.isEnabled(.exampleFeatureABTest))

通常,使用 Feature.enabled() 方法测试功能是否全局启用;在这种特定情况下,如果用户属于接收新功能的组,则返回 true,如果用户属于对照组,则返回 false。请注意,如果此功能的 JSON 中的 enabled 属性设置为 false,即测试全局禁用,则此方法也会返回 false

多变量 (MVT) 测试

多变量测试的配置与 A/B 测试的配置模式非常相似。将以下功能对象添加到 JSON 文件中的功能数组中

{
	"name": "Example MVT Test",
	"enabled": true,
	"test-variations": ["Group A", "Group B", "Group C"]
}

如果您向数组添加两个以上的测试变体,FeatureFlags 知道您正在配置 MVT 测试。同样,使用 Feature.Name 上的扩展将您的功能导入代码

extension Feature.Name {
	static let exampleMVTTest = Feature.Name(rawValue: "Example MVT Test")
}

使用以下代码来检查用户已被分配到哪个组

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())”)
}

您可以随意命名您的测试变体

{
	"name": "Example MVT Test",
	"enabled": true,
	"test-variations": ["Red", "Green", "Blue"]
}

只需在 Test.Variation 上创建一个扩展,以便在代码中映射您的测试变体

extension Test.Variation {
	static let red = Test.Variation(rawValue: "Red")
	static let green = Test.Variation(rawValue: "Green")
	static let blue = Test.Variation(rawValue: "Blue")
}

然后检查用户已被分配到哪个组

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())")
}

开发标志

当开发人员谈论功能开关时,他们通常指的是以下两种情况之一

对于开发标志,我们绝不希望将开发中的代码发布给用户。

考虑一下,如果我们使用远程功能开关来关闭未完成的功能,从而允许我们发布应用程序的版本 1 而不包含该功能。如果随后我们在应用程序的版本 2 中完成了该功能并打开了该功能,那么版本 1 的用户将体验到部分完成的功能,因为开发中的版本 1 代码已启用。这是我们绝不希望出现的情况,因此 FeatureFlags 既支持开发标志,也支持远程标志。

要将功能开关标记为开发标志,首先在您的应用程序中包含一个捆绑的 JSON 文件,其中包含 features 键。JSON 文件可以是现有文件或全新的文件。接下来,在将您的功能开关定义为此文件的一部分后,将配置 URL 设置为引用此文件

guard let featuresURL = Bundle.main.url(forResource: "features", withExtension: "json") else { return }
FeatureFlags.configurationURL = featuresURL

或者,如果您已经在使用远程配置 URL,则设置后备配置 URL

guard let fallbackURL = Bundle.main.url(forResource: "features", withExtension: "json") else { return }
FeatureFlags.localFallbackConfigurationURL = fallbackURL

像往常一样在 JSON 中设置您的功能开关,但将 development 属性设置为 true

{
    "features": [{
        "name": "Example Feature Flag",
        "development": true,
        "enabled": true
    }]
}

就这样!从现在开始,此功能开关将被视为开发标志,即使 enabled 设置为 true,其背后的代码也永远不会发布给用户。

在以下情况下将显示开发标志代码

如果您需要更精细地控制开发中的代码,那么您可以在检查功能是否启用时传递 isDevelopment 标志,例如

if let feature = Feature.named(.exampleFeatureFlag, isDevelopment: true) {
	print("Feature name -> \(feature.name)")
	print("Feature enabled -> \(feature.isEnabled())")
}

在此示例中,只有在 exampleFeatureFlag 启用且应用程序处于开发中(即设置了 #DEBUGFeatureFlags.isDevelopment)时,才会执行 print 语句。

解锁标志

无论功能开关是本地控制还是远程控制,上述类型的标志都在全局级别运行,即它们将为所有用户启用或禁用功能。但是,如果我们想在用户完成应用内购买或作为完成某些目标的奖励后为个别用户解锁功能,那么我们需要一种解锁功能并使其保持解锁状态的方法 - 我们通过解锁标志来实现这一点。

要在您的配置 JSON 文件中配置解锁标志,请添加以下内容

{
    "features": [{
        "name": "Example Unlock Flag",
        "enabled": true,
        "unlocked": false
    }]
}

unlocked 属性向 FeatureFlags 指示这将是一个解锁标志,其默认值为 false,即该功能最初将被锁定。请注意,如果 enabled 属性设置为 false,则该功能将对所有用户禁用,无论该功能是否已为特定用户解锁,因为此属性在全局级别运行。

可以查询解锁标志是否已解锁,如下所示

if let feature = Feature.named(.exampleUnlockFlag) {
    print("Is unlocked? -> \(feature.isUnlocked())")
}

当您希望为用户解锁功能时,请调用 feature.unlock()。相反,如果您只想在特定时间段内解锁功能,例如允许用户试用功能,您可以稍后调用 feature.lock() 以使该功能再次不可用。这两种方法都返回一个 Bool 值,以指示功能是否已解锁/锁定。

if let feature = Feature.named(.exampleUnlockFlag) {
    print("Is unlocked? -> \(feature.unlock())")
}

请注意,在以下情况下,功能可能无法解锁

高级用法

测试偏差

默认情况下,对于任何 A/B 或 MVT 测试,用户被分配到每个指定测试变体的可能性均等,即对于 A/B 测试,分配到一个组或另一个组的可能性为 50%/50%。对于具有四个变体的 MVT 测试,分配到每个变体的可能性为 25%。

可以配置测试偏差,以便分配到每个测试变体的可能性不相等。为此,只需将以下 JSON 添加到您的功能对象

{
	"features": [{
		"name": "Example A/B Test",
		"enabled": true,
		"test-biases": [80, 20],
		"test-variations": ["Group A", "Group B"]
	}]
}

test-biases 数组中指定的权重数量必须等于测试变体的数量,并且总和必须为 100,否则权重将被忽略并默认为相等权重。

标签

如果您希望发送与用户已被分配到的测试组相关的分析数据,则可以将标签附加到测试变体。

为此,请定义一个 labels 数组,其长度与指定的测试变体数量相等

{
	"features": [{
		"name": "Example A/B Test",
		"enabled": true,
		"test-biases": [50, 50],
		"test-variations": ["Group A", "Group B"],
		"labels": ["label1-for-analytics", "label2-for-analytics"]
	}]
}

然后在代码中检索您的标签,您将编写以下代码

if let feature = Feature.named(.exampleABTest) {
	print("Group A label -> \(feature.label(.groupA))")
	print("Group B label -> \(feature.label(.groupB))")
}

功能推广

FeatureFlags 框架最强大的功能是能够调整远程 JSON 配置文件中的测试偏差,并让用户自动重新分配到新的测试组。例如,您可能决定在第一周使用 10%/90% 的比例(其中 10% 的用户接收新功能)推广功能,第二周使用 20%/80% 的比例,依此类推。

只需更新 test-biases 数组中的权重,框架下次检查您的 JSON 配置时,组将重新分配。

当您完成功能的 A/B 或 MVT 测试后,您将收集足够的分析数据来决定是否将该功能推广到您的所有用户群。此时,您可以决定通过在 JSON 文件中将 enabled 标志设置为 false 来完全禁用该功能,或者在测试成功的情况下,您可以决定通过将 JSON 文件中的功能对象从

{
	"features": [{
		"name": "Example A/B Test",
		"enabled": true,
		"test-biases": [50, 50],
		"test-variations": ["Group A", "Group B"],
		"labels": ["label1-for-analytics", "label2-for-analytics"]
	}]
}

调整为以下为所有用户全局启用的功能开关,从而将该功能推广到所有用户

{
	"features": [{
		"name": "Example A/B Test",
		"enabled": true
	}]
}

质量保证 (QA)

为了测试您的新功能的两种变体是否都能正常工作,您可能需要在运行时调整功能开关/测试的状态。为此,FeatureFlags 提供了 FeatureFlagsViewController,允许您在应用程序的调试版本中打开/关闭功能开关或循环 A/B 测试或 MVT 测试变体。

要显示视图控制器,请指定所需的导航首选项,然后通过提供 UINavigationController 来推送视图控制器

 let navigationSettings = FeatureFlagsViewControllerNavigationSettings(autoClose: true, closeButtonAlignment: .right, closeButton: .save, isNavigationBarHidden: false)
 
FeatureFlags.pushFeatureFlags(delegate: self, navigationController: navigationController, navigationSettings: navigationSettings)

FeatureFlagsViewController

如果您需要有关每个功能开关/测试状态的更多信息,您可以使用 3D Touch 来预览/弹出更多信息。

FeatureDetailsViewController

刷新配置

如果您需要在任何时候刷新您的配置,您可以调用 FeatureFlags.refresh(),它可以选择接受一个完成闭包,以在刷新完成时通知您。

如果您选择将您的功能开关信息作为您的应用程序已获取的现有 JSON 文件的一部分包含在内,您可能希望使用以下方法传递 JSON 文件数据,以避免重复的网络调用

FeatureFlags.refreshWithData(_:completion:) 

Objective-C

虽然 FeatureFlags 主要用于 Swift 应用程序,但如果需要检查 Objective-C 中是否启用了功能开关,可以按如下方式操作

static NSString *const kMyNewFeatureFlag = @"My New Feature Flag";

if (FEATURE_IS_ENABLED(kMyNewFeatureFlag)) {
    ...
}

作者

Ross Butler

许可证

FeatureFlags 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件

附加软件

控件

AnimatedGradientView
AnimatedGradientView

框架

Cheats Connectivity FeatureFlags Skylark TypographyKit Updates
Cheats Connectivity FeatureFlags Skylark TypographyKit Updates

工具

Config Validator IPA Uploader Palette
Config Validator IPA Uploader Palette