---
title: "OpenFeature Swift Provider"
description: "ConfigDirector Provider for the OpenFeature Swift SDK"
canonical_url: "https://docs.configdirector.com/sdks/openfeature/swift"
---
# OpenFeature Swift Provider

> ConfigDirector Provider for the OpenFeature Swift SDK

<div hidden="true" dataTitleIcon="openfeature-swift">



</div>

<external-badges repo="configdirector-swift-openfeature-provider">



</external-badges>

## Introduction

The OpenFeature Swift Provider is intended to be used in combination with the [OpenFeature Swift SDK](https://openfeature.dev/docs/reference/sdks/client/swift/) in Apple platform applications. The provider wraps the ConfigDirector [Swift SDK](https://docs.configdirector.com/sdks/mobile/swift).

<table>
<thead>
  <tr>
    <th>
      Platform
    </th>
    
    <th>
      Minimum
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      iOS / iPadOS
    </td>
    
    <td>
      15.0
    </td>
  </tr>
  
  <tr>
    <td>
      macOS
    </td>
    
    <td>
      12.0
    </td>
  </tr>
  
  <tr>
    <td>
      tvOS
    </td>
    
    <td>
      15.0
    </td>
  </tr>
  
  <tr>
    <td>
      watchOS
    </td>
    
    <td>
      8.0
    </td>
  </tr>
</tbody>
</table>

Building it requires Swift 6.0 or newer and version 0.6.0 or newer of the OpenFeature Swift SDK. The provider is `Sendable`, so it can be registered from any task or actor.

Config evaluation is synchronous and reads config state the provider already holds in memory, which is what makes it safe to call from a SwiftUI `body`. Telemetry about those evaluations is aggregated and reported on its own task, so evaluating a config never waits on the network.

## Installation

The provider is distributed through Swift Package Manager from its repository: [https://github.com/ConfigDirector/configdirector-swift-openfeature-provider](https://github.com/ConfigDirector/configdirector-swift-openfeature-provider)

The ConfigDirector Swift SDK is included as a transitive dependency. The OpenFeature Swift SDK has to be added alongside the provider, since your application imports it to evaluate flags.

<code-group>

```bash [Xcode]
# In Xcode, go to File -> Add Package Dependencies... and enter the package URL:
https://github.com/ConfigDirector/configdirector-swift-openfeature-provider
```

```swift [Package.swift]
dependencies: [
    .package(url: "https://github.com/ConfigDirector/configdirector-swift-openfeature-provider", from: "1.5.0"),
    .package(url: "https://github.com/open-feature/swift-sdk", from: "0.6.0"),
],
targets: [
    .target(
        name: "YourTarget",
        dependencies: [
            .product(name: "ConfigDirectorOpenFeatureProvider", package: "configdirector-swift-openfeature-provider"),
            .product(name: "OpenFeature", package: "swift-sdk"),
        ]
    ),
]
```

</code-group>

## Configure and initialize the client

1. Create an instance of the provider using your client SDK key. You can retrieve a client SDK key for each environment under `SDK keys` in the dashboard's navigation panel.
2. Set the OpenFeature provider, along with the initial evaluation context.
3. Get a client instance from OpenFeature.

```swift [ConfigDirectorSetup.swift]
import ConfigDirectorOpenFeatureProvider
import OpenFeature

let provider = try ConfigDirectorProvider(clientSDKKey: "YOUR-CLIENT-SDK-KEY")
await OpenFeatureAPI.shared.setProviderAndWait(
    provider: provider,
    initialContext: ImmutableContext(targetingKey: "user-123")
)
let client = OpenFeatureAPI.shared.getClient()
```

The initializer throws a `ConfigDirectorError` only for a programming error, never for a runtime condition: `missingClientSDKKey` when the key is blank outside offline mode, `invalidBaseURL` when a custom base URL is not absolute, `valuesRequireOfflineMode` when values are given outside offline mode, and `invalidOfflineValue` when one of those values cannot be served. Connection failures are not thrown.

`setProviderAndWait` returns once the initial config state arrives or the configured timeout elapses. When it times out, the provider reports the `error` status and keeps connecting in the background, then reports `ready` as soon as config state arrives. Until then, flags resolve to their default values.

Most applications register the provider once during startup and let it live for the lifetime of the app. In a SwiftUI app that usually means doing so from the `App`:

```swift [MyApp.swift]
import ConfigDirectorOpenFeatureProvider
import OpenFeature
import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    guard let provider = try? ConfigDirectorProvider(clientSDKKey: "YOUR-CLIENT-SDK-KEY") else { return }
                    await OpenFeatureAPI.shared.setProviderAndWait(
                        provider: provider,
                        initialContext: ImmutableContext(targetingKey: "user-123")
                    )
                }
        }
    }
}
```

<note>

The underlying SDK follows the app lifecycle so it can release its network connection while the app is backgrounded. See the [`connection` options of the Swift SDK](https://docs.configdirector.com/sdks/mobile/swift#connection) to manage that yourself instead.

</note>

### Additional configuration options

Additional configuration options can be passed into the provider as `ConfigDirectorClientOptions` in the optional second argument of the initializer. The types that configure the underlying client are exported from the provider's module, so configuring it needs no second import.

For example, the `metadata` can be provided like this:

```swift [ConfigDirectorSetup.swift]
import ConfigDirectorOpenFeatureProvider

let provider = try ConfigDirectorProvider(
    clientSDKKey: "YOUR-CLIENT-SDK-KEY",
    options: ConfigDirectorClientOptions(
        metadata: ConfigDirectorMetaContext(
            appName: "YOUR-APP-NAME",
            appVersion: "1.0.2"
        )
    )
)
```

The provider accepts the same `metadata`, `connection`, `caching` and `logger` options as the Swift SDK client, [refer to the additional configuration options section of the Swift SDK for a full list](https://docs.configdirector.com/sdks/mobile/swift#additional-configuration-options).

That includes [running without a connection](https://docs.configdirector.com/sdks/mobile/swift#run-without-a-connection): created with `ConfigDirectorProvider(options:)`, the connection `mode` set to `.offline` and no client SDK key, the provider serves the values you give it without connecting to ConfigDirector, `setProviderAndWait` returns at once with the provider ready, and creating it throws when those values cannot be used. The provider's module exports `ServedValue`, the type of those values.

### Values kept on the device

By default, the provider keeps the last values it received from ConfigDirector on the device, in the app's `UserDefaults`, and serves them on the next launch of the same context until live values arrive. An app opened without a network, after at least one successful connection, resolves flags to the values ConfigDirector last sent instead of the default values passed to the OpenFeature client.

Registering the provider still waits for live values or the connection `timeout`: `setProviderAndWait` returns after the timeout as it does without kept values, and the provider then reports the `stale` status, with a `stale` event, while it serves the kept values. It reports `stale` from the moment the kept values are loaded, and again right after the `error` event a timeout reports. Once live values arrive, the provider reports `ready` with a `ready` event and the kept values are replaced. A context change that cannot connect reports `reconciling`, an `error` and then `stale` again, and keeps serving the current values. Resolutions carry no reason, as before.

The values are kept for the context they were evaluated for, and served on the next launch only when the evaluation context has the same `kind` and targeting key. A context without a targeting key matches on the same install, as long as the app has not been uninstalled, because ConfigDirector derives its identifier from the instance ID the SDK stores; such a context keeps its percentage rollout bucket for as long as that instance ID lasts. Other attributes take no part in the match: live values correct them within a second when there is a network. A context change keeps serving the current values until the new context's values arrive, as it does with live values.

Turn it off, or supply your own store, with the `caching` option of `ConfigDirectorClientOptions`, which takes the same `CachingOptions` the [Swift SDK](https://docs.configdirector.com/sdks/mobile/swift#values-kept-on-the-device) takes: `enabled`, defaulting to `true`, and `provider`, a `CachingProvider` with `read()`, `write(_:)`, `remove()` and `sizeLimit()` in bytes, defaulting to the app's `UserDefaults` under a key derived from the client SDK key with a size limit of 256 KB. The provider's module exports both types. The entry is one string whose format belongs to the SDK and may change between SDK versions. An entry above the size limit is removed with one warning, a store that fails is logged once and never stops the provider, and nothing is kept in `.offline` mode.

```swift [ConfigDirectorSetup.swift]
let provider = try ConfigDirectorProvider(
    clientSDKKey: "YOUR-CLIENT-SDK-KEY",
    options: ConfigDirectorClientOptions(
        caching: CachingOptions(enabled: false)
    )
)
```

### Shut down

The OpenFeature Swift SDK does not shut providers down. The provider closes its connection, and reports whatever telemetry is left, when it is released, which happens after `OpenFeatureAPI.shared.clearProvider()` as long as you hold no other reference to it. To close it while you still hold one, call `provider.close()`. The provider cannot be used afterwards.

## Retrieve config values

To retrieve config values, use the OpenFeature client:

```swift [ContentView.swift]
let booleanValue = client.getBooleanValue(key: "my-config-key", defaultValue: false)

let stringValue = client.getStringValue(key: "my-string-config-key", defaultValue: "Default")
```

Each OpenFeature getter maps to a ConfigDirector config type:

<table>
<thead>
  <tr>
    <th>
      OpenFeature getter
    </th>
    
    <th>
      ConfigDirector config value
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        getBooleanValue
      </code>
    </td>
    
    <td>
      Boolean
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        getStringValue
      </code>
    </td>
    
    <td>
      String or enum
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        getIntegerValue
      </code>
      
      , <code>
        getDoubleValue
      </code>
    </td>
    
    <td>
      Number
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        getObjectValue
      </code>
    </td>
    
    <td>
      JSON object or JSON array
    </td>
  </tr>
</tbody>
</table>

`getIntegerValue` takes and returns an `Int64`, as the OpenFeature Swift SDK defines it. `getObjectValue` returns the parsed JSON as an OpenFeature `Value`, a `.structure` for a JSON object or a `.list` for a JSON array:

```swift [ContentView.swift]
let settings = client.getObjectValue(key: "my-json-config-key", defaultValue: .structure([:]))
let theme = settings.asStructure()?["theme"]?.asString()
```

For additional information regarding the OpenFeature client [refer to the OpenFeature Swift SDK documentation](https://openfeature.dev/docs/reference/sdks/client/swift/).

### Reading configs in SwiftUI

The getters are synchronous and make no network calls, so they are safe to call directly from a `body`. That gives you the current value, but the view will not re-render on its own when the value changes.

To have the view follow a config, re-read it whenever the provider publishes an event. `OpenFeatureAPI.shared.observe()` is a Combine publisher of provider events that replays the provider's current status to a new subscriber, so the view reads the current value as soon as it appears, and again after every change:

```swift [DarkModeToggle.swift]
import OpenFeature
import SwiftUI

struct DarkModeToggle: View {
    @State private var isOn = false

    var body: some View {
        Toggle("Dark mode", isOn: $isOn)
            .onReceive(OpenFeatureAPI.shared.observe().receive(on: DispatchQueue.main)) { _ in
                isOn = OpenFeatureAPI.shared.getClient().getBooleanValue(key: "dark-mode", defaultValue: false)
            }
    }
}
```

## Update the context

```swift [ContentView.swift]
await OpenFeatureAPI.shared.setEvaluationContextAndWait(
    evaluationContext: ImmutableContext(
        targetingKey: "12345", // In OpenFeature, the targeting key represents the context's identifier
        structure: ImmutableStructure(attributes: [
            "name": .string("Example User"),
            // Any arbitrary traits which can be referenced in targeting rules
            "traits": .structure(["region": .string("North America")]),
        ])
    )
)
```

To evaluate flags for an entity other than a user, such as a tenant, set the context kind with a top-level `kind` attribute:

```swift [ContentView.swift]
await OpenFeatureAPI.shared.setEvaluationContextAndWait(
    evaluationContext: ImmutableContext(
        targetingKey: "acme",
        structure: ImmutableStructure(attributes: [
            "kind": .string("tenant"),
            "name": .string("Acme Inc."),
            "traits": .structure(["plan": .string("enterprise")]),
        ])
    )
)
```

Leaving `kind` out means `user`. A `kind` inside `traits` is an ordinary trait, not the context kind. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and flags still resolve successfully, as if no context had been given. Context kinds are available from version 1.3.0 of the provider.

The evaluation context maps onto the ConfigDirector context as follows:

<table>
<thead>
  <tr>
    <th>
      OpenFeature evaluation context
    </th>
    
    <th>
      ConfigDirector context
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      The targeting key, or otherwise an <code>
        id
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        id
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The top-level <code>
        kind
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        kind
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The <code>
        name
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        name
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The <code>
        traits
      </code>
      
       structure attribute
    </td>
    
    <td>
      <code>
        traits
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The boolean <code>
        anonymous
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        anonymous
      </code>
    </td>
  </tr>
</tbody>
</table>

Any other attribute is ignored. Put the values your targeting rules depend on inside `traits`.

<note>

In client SDKs (browser and mobile), updating the context re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior context.<br />


Awaiting `setEvaluationContextAndWait` will wait until the new config values are downloaded or the connection times out. In the case of a timeout, the provider will continue to attempt to connect with the new context in the background.

</note>

For additional information regarding the OpenFeature client [refer to the OpenFeature Swift SDK documentation](https://openfeature.dev/docs/reference/sdks/client/swift/).

## Events

The provider publishes its events through `OpenFeatureAPI.shared.observe()`. It emits `configurationChanged` whenever configs are updated on the dashboard or via the admin API, carrying the keys of the configs in the update:

```swift [ConfigDirectorSetup.swift]
import Combine
import OpenFeature

let subscription = OpenFeatureAPI.shared.observe().sink { event in
    switch event {
    case let .configurationChanged(details):
        print("Configs updated: \(details?.flagsChanged ?? [])")
    case .ready:
        print("Config state is available")
    case let .error(details):
        print("ConfigDirector could not be reached: \(details?.message ?? "")")
    default:
        break
    }
}
```

It emits `ready` when the initial config state arrives after `setProviderAndWait` has already returned, `error` when the initial connection times out, `stale` while the [values kept on the device](#values-kept-on-the-device) are served, and `reconciling` followed by `contextChanged` around a context update.

## Test your code

Code that reads flags through OpenFeature is tested by swapping the provider, so it never has to change for a test. The OpenFeature Swift SDK ships no in-memory provider yet, so this provider accepts a **test client** from the [Swift SDK's `ConfigDirectorTesting` product](https://docs.configdirector.com/sdks/mobile/swift#test-your-code) instead: the SDK's real client connected to an in-memory server your test controls. The provider then resolves every flag from the values the test sets, opens no network connection, and sends no telemetry.

Swift Package Manager exposes only the products of packages your target declares, so add the ConfigDirector Swift SDK to your test target alongside the provider:

```swift [Package.swift]
dependencies: [
    .package(url: "https://github.com/ConfigDirector/configdirector-swift-openfeature-provider", from: "1.5.0"),
    .package(url: "https://github.com/ConfigDirector/configdirector-swift-sdk", from: "1.10.1"),
    .package(url: "https://github.com/open-feature/swift-sdk", from: "0.6.0"),
],
targets: [
    .testTarget(
        name: "YourAppTests",
        dependencies: [
            "YourApp",
            .product(name: "ConfigDirectorTesting", package: "configdirector-swift-sdk"),
        ]
    ),
]
```

In an Xcode project, add the `configdirector-swift-sdk` package and link its `ConfigDirectorTesting` library to your test target under **Frameworks and Libraries**.

The initializer that takes a client is `init(injectedClient:)`, and it is visible only to files that import the provider with `@_spi(Testing)`, which keeps it out of application code:

```swift [CheckoutTests.swift]
import ConfigDirectorTesting
@_spi(Testing) import ConfigDirectorOpenFeatureProvider
import OpenFeature
import Testing

struct CheckoutTests {
    @Test func showsTheNewCheckout() async {
        let testClient = makeTestClient(values: ["new-checkout": true, "max-items": 20])
        defer { testClient.client.close() }
        await OpenFeatureAPI.shared.setProviderAndWait(
            provider: ConfigDirectorProvider(injectedClient: testClient.client),
            initialContext: ImmutableContext(targetingKey: "user-123")
        )
        let client = OpenFeatureAPI.shared.getClient()

        #expect(client.getBooleanValue(key: "new-checkout", defaultValue: false))
        #expect(client.getIntegerValue(key: "max-items", defaultValue: 0) == 20)

        testClient.setValue(false, for: "new-checkout")
        #expect(!client.getBooleanValue(key: "new-checkout", defaultValue: false))
    }
}
```

`OpenFeatureAPI.shared` is one global, so tests that register their own provider on it cannot run in parallel. When the code under test takes an `OpenFeatureAPI` or a `Client` rather than reading the shared instance, create an `OpenFeatureAPI()` per test instead.

The test client's controls are the same as with the Swift SDK: `setValue(_:for:)`, `removeValue(for:)`, `replaceValues(_:)`, the initialization and context update holds and failures, and `contextUpdates`. [The Swift SDK's testing section](https://docs.configdirector.com/sdks/mobile/swift#test-your-code) describes each of them and the values they take. Through the provider:

- `setValue` makes the OpenFeature client resolve the new value at once and the provider emit `configurationChanged` with that key; `removeValue` makes the flag resolve to its default value.
- The provider initializes the client when it is registered and updates the client's context whenever the evaluation context changes, as it does with a client it creates itself. A test client that was already initialized therefore reconnects on registration, and its `contextUpdates` records the provider's context after the test's own.
- `holdInitialization()` before registering keeps `setProviderAndWait` waiting, with the provider `notReady` and flags at their default values, until `completeInitialization()` or the test client's `timeout` elapses. A timed-out or failed initialization (`failInitialization()`) makes the provider report the `error` status, as a connection failure would, and flags resolve to their default values.
- The provider never closes a client it was given, whether through `close()` or when it is released. Close the test client's `client` when the test is done, as in the example.


## Sitemap

See the full [sitemap](https://docs.configdirector.com/sitemap.md) for all pages.
