# iOS SDK 中的功能开关

`Pushwoosh.configure.featureFlags` 为您的 iOS 应用提供在 Control Panel 中为该应用配置的[功能开关](/zh/product/audience-data-and-segmentation/feature-flags/)。Pushwoosh 在服务器上针对当前设备和用户评估开关规则，SDK 返回评估结果：该开关是否对此设备开启，以及适用哪些属性值。

<Aside type="tip" title="Beta 功能">
功能开关目前处于 Beta 阶段，仅在包含该功能的套餐中可用。在其他套餐中，服务器不返回任何开关，因此每个开关都会回退到您在代码中传入的默认值。
</Aside>

## 前提条件

* Pushwoosh iOS SDK 7.2.10 或更高版本，并按照 [iOS SDK 概述](/zh/developer/pushwoosh-sdk/ios-sdk/)中的说明完成集成。功能开关包含在必需的 `PushwooshFramework` 和 `PushwooshCore` 模块中，因此无需添加其他模块。功能开关仅在 iOS 应用本身中工作。应用扩展（例如 Notification Service Extension）从不请求开关。
* 在 **Audience** → **Feature Flags** 中至少创建了一个开关。您需要它的 **Key** 以及其属性的键和类型。请严格按照其中显示的形式使用这些键，包括大小写。例如，`get("New_Checkout")` 找不到键为 `new_checkout` 的开关。
* 如果应用通过反向代理发送 SDK 请求，代理 URL 必须以 `json/1.3/` 结尾。否则 SDK 不会请求开关，每个开关都会回退到您的默认值。

## 读取开关

请始终传入默认值：在 SDK 尚未获得该设备的开关之前，以及当开关不存在、已归档或套餐不包含功能开关时，都会使用该默认值。

```swift
import PushwooshFramework

let flags = Pushwoosh.configure.featureFlags

if let checkout = flags.get("new_checkout"), checkout.isEnabled {
    showNewCheckout(
        buttonColor: checkout.string(forProperty: "button_color", default: "blue") ?? "blue",
        maxItems: Int(checkout.number(forProperty: "max_items", default: 3))
    )
} else {
    showOldCheckout()
}
```

如果只需检查开关是否开启，请使用 `isEnabled`：

```swift
let isNewCheckout = Pushwoosh.configure.featureFlags.isEnabled("new_checkout", default: false)
```

在 Objective-C 中，请调用 `[PushwooshConfig featureFlags]`。

## 开关返回的内容

`get(_:)` 返回一个 `PWFeatureFlag` 对象；当 SDK 尚未获得该设备的开关，或响应中不包含该开关时，返回 `nil`。已归档的开关不会出现在响应中。`get` 仅读取存储的开关，从不等待网络。该对象包含以下属性：

| 属性 | 类型 | 描述 |
| :---- | :---- | :---- |
| `key` | String | 开关键。 |
| `isEnabled` | Bool | 该开关是否对此设备开启。 |
| `ruleId` | String | 决定结果的规则，例如 `r_1a2b3c4d`。没有规则匹配该设备时为 `default`，开关被终止（killed）时为空字符串。 |

请使用与属性类型匹配的方法读取属性值。当开关关闭时，属性保存的是在 Control Panel 中设置的默认值。

| 方法 | 返回值 |
| :---- | :---- |
| `string(forProperty:default:)` | **String** 属性的值，或您的默认值。 |
| `number(forProperty:default:)` | **Number** 属性的值（`Double` 类型），或您的默认值。 |
| `bool(forProperty:default:)` | **Boolean** 属性的值，或您的默认值。 |
| `jsonObject(forProperty:)` | 存放对象的 **JSON** 属性的值，以字典形式返回，或 `nil`。 |
| `jsonArray(forProperty:)` | 存放数组的 **JSON** 属性的值，或 `nil`。 |

当属性缺失或其值属于其他类型时，每个方法都会返回您的默认值；JSON 方法则返回 `nil`。

当 `get(_:)` 返回 `nil` 时，`isEnabled(_:default:)` 返回默认值。

<span id="keep-flags-up-to-date" />

## 保持开关最新

SDK 会将最近一次响应存储在设备上，最长 24 小时。存储的开关仅对请求它们时的 User ID 和应用的 Application Code 有效。SDK 会在以下情况下刷新开关：

* 会话开始时，即应用首次变为活跃状态时，或在后台停留至少 30 分钟之后；
* 应用处于前台期间每小时一次；
* 设备注册推送通知后，如果上一次请求是在注册之前发送的；
* 通过 `setUserId` 变更 User ID 后立即刷新。上一个用户的存储开关会被丢弃，因此新用户永远不会看到它们。

要响应新值，请添加一个遵循 `PWFeatureFlagsUpdateListener` 协议的监听器。每次刷新改变了任一开关后，都会在主线程上调用它的 `featureFlagsDidUpdate()`。SDK 以弱引用方式持有监听器。请对您的监听器保持强引用，否则它会被释放，永远不会被调用：

```swift
final class CheckoutViewController: UIViewController, PWFeatureFlagsUpdateListener {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        Pushwoosh.configure.featureFlags.addUpdateListener(self)
    }

    override func viewWillDisappear(_ animated: Bool) {
        Pushwoosh.configure.featureFlags.removeUpdateListener(self)
        super.viewWillDisappear(animated)
    }

    func featureFlagsDidUpdate() {
        render()
    }
}
```

要自行请求开关，例如在某个可能使用户进入不同 Segment 的操作之后，请调用 `refresh(completion:)`。在存储的开关有效期间，它每 5 分钟最多发送一次请求，并在该时间窗口内直接使用存储的开关作答。completion 在主线程上运行：

```swift
Pushwoosh.configure.featureFlags.refresh { error in
    if let error = error as NSError? {
        let status = error.code
        // the stored flags, or your defaults, stay in effect
    }
    render()
}
```

失败时，错误的 domain 为 `PWFeatureFlagsErrorDomain`，其 `code` 为 HTTP 状态码。当请求没有收到 HTTP 响应或未发送时，code 为 `0`；当服务器已响应但响应无法使用时，code 为 `200`。

要在不受 5 分钟限制的情况下测试刷新，请在 `Info.plist` 中将 Boolean 类型的 `Pushwoosh_FEATURE_FLAGS_DISABLE_THROTTLE` 键设置为 `YES`。

如果 Pushwoosh 无法及时匹配设备所属的 Segment，它会返回部分响应。SDK 不会将其存储在设备上，下一次刷新会带来完整响应。

当某个开关在 Control Panel 中被终止（killed）时，设备会在下一次刷新时获得关闭状态的开关及默认属性值。当 SDK 无法连接服务器时，它会继续使用存储的开关，直到它们存在满 24 小时，此后使用您的默认值。

<span id="log-impressions" />

## 记录曝光

曝光（impression）告诉 Pushwoosh，用户确实执行到了某个开关所控制的代码，以便您对比获得该功能的用户与未获得该功能的用户。请在用户看到该功能或其旧版本的那一刻调用 `logImpression(_:)`，开启分支和关闭分支都要调用，因为开关关闭的用户就是您的对比组：

```swift
let flags = Pushwoosh.configure.featureFlags
let isOn = flags.isEnabled("new_checkout", default: false)
flags.logImpression("new_checkout")
if isOn {
    showNewCheckout()
} else {
    showOldCheckout()
}
```

SDK 会发送带有以下属性的 `PW_FeatureFlagImpression` 事件：

| 属性 | 类型 | 值 |
| :---- | :---- | :---- |
| `flag_key` | string | 开关键。 |
| `rule_id` | string | 决定结果的规则，与 `ruleId` 相同。 |
| `enabled` | boolean | 该开关当时是否开启。 |

您无需事先创建该事件，因为当第一条曝光到达时，Pushwoosh 会将带有这些属性的 `PW_FeatureFlagImpression` 添加到 **Audience** → **Events** 中。SDK 在一个会话中针对每个开关、规则和状态只发送一次曝光，当该键没有对应开关时不会发送任何内容。如果发送失败，下一次针对该开关调用 `logImpression` 时会重新发送。

曝光还可以让营销人员构建一个已触达该功能的用户 Segment：即[功能开关条件](/zh/product/audience-data-and-segmentation/feature-flags/#find-users-who-reached-a-feature)中的 **看到过该功能（30 天）** 状态。

## 方法参考

`Pushwoosh.configure.featureFlags` 包含以下方法：

| 方法 | 描述 |
| :---- | :---- |
| `get(_:)` | 返回该键对应的 `PWFeatureFlag`，或 `nil`。仅读取存储的开关。 |
| `isEnabled(_:default:)` | 返回该开关是否开启；没有开关时返回默认值。 |
| `refresh(completion:)` | 请求开关；在存储的开关有效期间，限制为每 5 分钟一次。completion 收到 `nil` 或 `PWFeatureFlagsErrorDomain` 中的错误，completion 本身可以为 `nil`。 |
| `addUpdateListener(_:)` | 每次刷新改变了开关后调用监听器的 `featureFlagsDidUpdate()`。监听器以弱引用方式持有。 |
| `removeUpdateListener(_:)` | 移除通过 `addUpdateListener(_:)` 添加的监听器。 |
| `logImpression(_:)` | 针对该开关当前的规则和状态发送 `PW_FeatureFlagImpression` 事件。 |