# Feature flags in the iOS SDK

`Pushwoosh.configure.featureFlags` gives your iOS app the [feature flags](/product/audience-data-and-segmentation/feature-flags/) configured for the application in the Control Panel. Pushwoosh evaluates the flag rules on the server for the current device and user, and the SDK returns the result: whether the flag is on for this device and which property values apply.

<Aside type="tip" title="Beta feature">
Feature flags are in beta and available only on plans that include them. On other plans the server returns no flags, so every flag falls back to the default you pass in code.
</Aside>

## Prerequisites

* Pushwoosh iOS SDK 7.2.10 or later, integrated as described in the [iOS SDK guide](/developer/pushwoosh-sdk/ios-sdk/). Feature flags ship in the required `PushwooshFramework` and `PushwooshCore` modules, so you don't add another module. They work in the iOS app itself. App extensions, such as a Notification Service Extension, never request flags.
* At least one flag created in **Audience** → **Feature Flags**. You need its **Key** and the keys and types of its properties. Use the keys exactly as they appear there, letter case included. For example, `get("New_Checkout")` doesn't find a flag with the key `new_checkout`.
* If the app sends SDK requests through a reverse proxy, its URL must end with `json/1.3/`. Otherwise the SDK doesn't request flags, and every flag falls back to your default.

## Read a flag

Always pass a default. It applies until the SDK has flags for the device, and whenever the flag doesn't exist, is archived, or the plan doesn't include feature flags.

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

To check only whether the flag is on, use `isEnabled`:

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

In Objective-C, call `[PushwooshConfig featureFlags]`.

## What a flag returns

`get(_:)` returns a `PWFeatureFlag` object, or `nil` when the SDK has no flags for the device yet or the flag isn't in the answer. An archived flag is not in the answer. `get` reads only the stored flags and never waits for the network. The object has these properties:

| Property | Type | Description |
| :---- | :---- | :---- |
| `key` | String | The flag key. |
| `isEnabled` | Bool | Whether the flag is on for this device. |
| `ruleId` | String | The rule that decided the result, for example `r_1a2b3c4d`. `default` when no rule matched the device, and an empty string when the flag is killed. |

Read property values with the method that matches the property type. When the flag is off, the properties hold the default values set in the Control Panel.

| Method | Returns |
| :---- | :---- |
| `string(forProperty:default:)` | The value of a **String** property, or your default. |
| `number(forProperty:default:)` | The value of a **Number** property as a `Double`, or your default. |
| `bool(forProperty:default:)` | The value of a **Boolean** property, or your default. |
| `jsonObject(forProperty:)` | The value of a **JSON** property that holds an object, as a dictionary, or `nil`. |
| `jsonArray(forProperty:)` | The value of a **JSON** property that holds an array, or `nil`. |

Each method returns your default, or `nil` for the JSON methods, when the property is missing or holds a value of another type.

`isEnabled(_:default:)` returns the default when `get(_:)` returns `nil`.

## Keep flags up to date

The SDK stores the last answer on the device for up to 24 hours. The stored flags are valid only for the User ID and the Application Code of the app they were requested for. The SDK refreshes them:

* When a session starts, that is, when the app becomes active for the first time or after at least 30 minutes in the background.
* Every hour while the app is in the foreground.
* After the device registers for push notifications, if the previous request was sent before the registration.
* Right after the User ID changes with `setUserId`. The stored flags of the previous user are discarded, so a new user never sees them.

To react to new values, add a listener that adopts `PWFeatureFlagsUpdateListener`. Its `featureFlagsDidUpdate()` is called on the main thread after a refresh that changed any flag. The SDK holds listeners weakly. Keep a strong reference to yours, or it's released and never called:

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

To request the flags yourself, for example after an action that may move the user into a different segment, call `refresh(completion:)`. While the stored flags are valid, it sends at most one request per 5 minutes and answers from the stored flags within that window. The completion runs on the main thread:

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

On failure, the error has the `PWFeatureFlagsErrorDomain` domain, and its `code` is the HTTP status. The code is `0` when the request got no HTTP response or wasn't sent, and `200` when the server answered but the answer couldn't be used.

To test refreshes without the 5-minute limit, set the `Pushwoosh_FEATURE_FLAGS_DISABLE_THROTTLE` Boolean key in `Info.plist` to `YES`.

If Pushwoosh can't match the device's segments in time, it returns a partial answer. The SDK doesn't store it on the device, and the next refresh brings the full answer.

When a flag is killed in the Control Panel, devices get it off with default property values at the next refresh. When the SDK can't reach the server, it keeps using the stored flags until they are 24 hours old, and your defaults after that.

## Log impressions

An impression tells Pushwoosh that a user actually reached the code a flag controls, so you can compare users who got the feature with those who didn't. Call `logImpression(_:)` at the moment the user sees the feature or its old version, for both the enabled and the disabled branch, because users with the flag off are your comparison group:

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

The SDK posts the `PW_FeatureFlagImpression` event with these attributes:

| Attribute | Type | Value |
| :---- | :---- | :---- |
| `flag_key` | string | The flag key. |
| `rule_id` | string | The rule that decided the result, as in `ruleId`. |
| `enabled` | boolean | Whether the flag was on. |

You don't need to create the event in advance, because Pushwoosh adds `PW_FeatureFlagImpression` with these attributes to **Audience** → **Events** when the first impression arrives. The SDK sends one impression per flag, rule, and state in a session, and sends nothing when there is no flag for the key. If sending fails, the next `logImpression` call for the flag sends it again.

Impressions also let marketers build a segment of users who reached the feature: the **saw the feature (30 days)** state of the [feature flag condition](/product/audience-data-and-segmentation/feature-flags/#find-users-who-reached-a-feature).

## Method reference

`Pushwoosh.configure.featureFlags` has these methods:

| Method | Description |
| :---- | :---- |
| `get(_:)` | Returns the `PWFeatureFlag` for the key, or `nil`. Reads the stored flags only. |
| `isEnabled(_:default:)` | Returns whether the flag is on, or the default when there is no flag. |
| `refresh(completion:)` | Requests the flags, throttled to once per 5 minutes while the stored flags are valid. The completion gets `nil` or an error in `PWFeatureFlagsErrorDomain`, and can be `nil`. |
| `addUpdateListener(_:)` | Calls the listener's `featureFlagsDidUpdate()` after every refresh that changed a flag. The listener is held weakly. |
| `removeUpdateListener(_:)` | Removes a listener added with `addUpdateListener(_:)`. |
| `logImpression(_:)` | Posts a `PW_FeatureFlagImpression` event for the flag's current rule and state. |