功能标志 3.0.0

功能标志 3.0.0

Ross Butler 维护。




  • 作者
  • Ross Butler

FeatureFlags

Build Status Version Carthage compatible Maintainability License Reviewed by Hound

FeatureFlags 通过一个可能捆绑在您的应用程序中或远程托管的自定义 JSON 文件,使配置功能标志、A/B 和 MVT 测试变得容易。对于远程托管的配置文件,您可以在不发布新版本到 App Store 的情况下启用/禁用功能,更新 A/B 测试组中用户的百分比,或者在您决定功能已准备好发布时,将先前处于 A/B 测试中的功能推广到 100% 的用户。

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

功能

  • 功能标志
  • A/B 测试和 MVT 测试
  • 功能 A/B 测试(某个功能启用与未启用该功能的控制组之间的比较)
  • 远程托管功能标志 JSON 配置,使您能够在不发布新版本应用程序的情况下启用/禁用功能
  • 使用现有的 JSON 文件或托管完全新的配置
  • 远程调整每个测试组中用户的百分比
  • 一旦您决定功能测试是否成功,即可将 A/B 测试转换为功能标志,即向 100% 的用户推广功能
  • 使用您的应用程序调试构建中的 FeatureFlagsViewController 可视化标志和测试的状态

特性标志何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 包管理者

Swift 包管理者是一个用于 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文件中的features数组中

{
	"name": "Example A/B Test",
	"enabled": true,
	"test-variations": ["Group A", "Group B"]
}
  • enabled表示是否启用A/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测试中 - 特性要么开启,要么关闭。

使用以下JSON配置特性A/B测试

{
	"name": "Example Feature A/B Test",
	"enabled": true,
	"test-variations": ["Enabled", "Disabled"]
}
  • enabled表示是否启用A/B测试。
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中设置为falseenabled属性时返回false,即测试全局禁用。

多元(MVT)检验

配置多元检验的模式类似于A/B测试。请将以下特性对象添加到JSON文件中的features数组

{
	"name": "Example MVT Test",
	"enabled": true,
	"test-variations": ["Group A", "Group B", "Group C"]
}
  • enabled 表示MVT测试是否启用。

如果你向数组中添加了超过两种测试变体,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"]
}
  • enabled 表示MVT测试是否启用。

简单地在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不仅支持开发标志,也支持远程标志。

要将一个特性标志标记为开发标志,首先包括一个包含features键的捆绑JSON文件作为你的应用程序的一部分。该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的情况下也永远不会发布给用户。

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

  • 如果设置了#DEBUG预处理器标志且标志已启用。
  • 如果将 FeatureFlags.isDevelopment 设置为 true(开发者负责在应用开发时将其设置为 true - 默认值为 false)并且该标志处于 启用 状态。

如果您需要更细粒度地控制开发中的代码,那么可以在检查是否启用某个功能时传递 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 属性表示此标志是解锁标志,其默认值为 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())")
}

注意,以下情况可能会导致功能无法解锁:

  • 功能标志不是解锁标志,即 JSON 中未定义 unlocked 属性。
  • enabled 属性设置为 false

高级用法

测试偏差

默认情况下,对于任何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 - 现代iOS应用的后现代作弊码。
  • Connectivity - 改进了Reachability以确定你的iOS应用中的Internet连接性。
  • FeatureFlags - 允许开发者为功能标志进行配置,通过打包或从远程服务器托管JSON配置文件来运行多个A/B或MVT测试。
  • FlexibleRowHeightGridLayout - 一个为支持动态类型而设计的UICollectionView网格布局,允许每一行的尺寸适应内容。
  • Hash - 使用流行的散列函数生成消息摘要和HMAC的轻量级方法,包括MD5、SHA-1、SHA-256。
  • Skylark - 一个完全使用Swift编写的BDD测试框架,用于使用Gherkin语法编写Cucumber场景。
  • TailorSwift - 一组有用的 Swift 核心库/Foundation 框架扩展。
  • TypographyKit - 支持动态字体,iOS 上具有一致的易于访问的视觉样式。
  • Updates - 自动检测应用程序更新,并温柔地提示用户更新。
快捷键 连接性 功能标志 Skylark TypographyKit Updates
Cheats Connectivity FeatureFlags Skylark TypographyKit Updates

工具

  • Clear DerivedData - 通过键盘输入 cdd 从终端快速清除您的 DerivedData 目录。
  • Config Validator - Config Validator 验证并上传您的配置文件,作为 CI 流程的一部分清除 CDN 缓存。
  • IPA Uploader - 将您的应用程序上传到 TestFlight 和 App Store。
  • Palette - 使您的 TypographyKit 颜色调色板在 Xcode Interface Builder 中可用。
Config Validator IPA Uploader Palette
Config Validator IPA Uploader Palette