Skip to main content

OpenFeature iOS SDK

SpecificationRelease
Status

Quick startโ€‹

MCP Install

Follow the MCP Getting Started guide to quickly set up the OpenFeature MCP server and connect your AI tool.

  • Run this prompt: "Install OpenFeature into this app"

Quick Install:

claude mcp add --transport stdio openfeature npx -y @openfeature/mcp

Requirementsโ€‹

This SDK supports the following Apple platforms:

  • iOS 15+
  • macOS 12+
  • watchOS 8+
  • tvOS 15+

The SDK is built with Swift 5.5+ and uses only Foundation and Combine, making it suitable for all Apple platform contexts including mobile, desktop, wearable, and TV applications. It has no third-party dependencies. An optional OpenFeatureSwiftLog product integrates with swift-log for those who use it (see Logging).

Installโ€‹

Xcode Dependenciesโ€‹

You have two options, both start from File > Add Packages... in the code menu.

First, ensure you have your GitHub account added as an option (+ > Add Source Control Account...). You will need to create a Personal Access Token with the permissions defined in the Xcode interface.

  1. Add as a remote repository
    • Search for git@github.com:open-feature/swift-sdk.git and click "Add Package"
  2. Clone the repository locally
    • Clone locally using your preferred method
    • Use the "Add Local..." button to select the local folder

Note: Option 2 is only recommended if you are making changes to the client SDK.

Swift Package Managerโ€‹

If you manage dependencies through SPM, in the dependencies section of Package.swift add:

.package(url: "git@github.com:open-feature/swift-sdk.git", from: "0.6.0")

and in the target dependencies section add:

.product(name: "OpenFeature", package: "swift-sdk"),

To log through swift-log, also add the optional bridge product:

.product(name: "OpenFeatureSwiftLog", package: "swift-sdk"),

iOS Usageโ€‹

import OpenFeature

Task {
let provider = CustomProvider()
// configure a provider, wait for it to complete its initialization tasks
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)

// get a bool flag value
let client = OpenFeatureAPI.shared.getClient()
let flagValue = client.getBooleanValue(key: "boolFlag", defaultValue: false)
}

Privacy Manifestโ€‹

The SDK ships a privacy manifest (PrivacyInfo.xcprivacy) that Swift Package Manager bundles automatically, so it is picked up by Xcode's privacy report and App Store submission checks.

The manifest declares that the SDK:

  • does not track users (NSPrivacyTracking is false) and contacts no tracking domains,
  • does not collect any data types on its own,
  • does not call any required reason APIs.

The SDK is an abstraction layer, it holds the evaluation context and tracking events your app supplies and hands them to the configured provider, but never persists or transmits them itself. Any data collection, tracking or required-reason API usage happens in the provider (or hook) you install. Third-party providers and hooks are responsible for declaring that in their own privacy manifest, so check the manifest of each one you use. Your app's own manifest must cover data collection, tracking, and required-reason API use by your app's code, including any custom providers or hooks you write yourself.

Featuresโ€‹

StatusFeaturesDescription
โœ…ProvidersIntegrate with a commercial, open source, or in-house feature management tool.
โœ…TargetingContextually-aware flag evaluation using evaluation context.
โœ…HooksAdd functionality to various stages of the flag evaluation life-cycle.
โœ…TrackingAssociate user actions with feature flag evaluations.
โœ…LoggingIntegrate with popular logging packages or your own logger.
โŒDomainsLogically bind clients with providers.
โœ…MultiProviderCombine multiple providers with configurable evaluation strategies.
โœ…InMemoryProviderResolve flags from an in-memory configuration, for demos and testing.
โœ…EventingReact to state changes in the provider or flag management system.
โŒShutdownGracefully clean up a provider during application shutdown.
โœ…ExtendingExtend OpenFeature with custom providers and hooks.
Implemented: โœ… | In-progress: โš ๏ธ | Not implemented yet: โŒ

Providersโ€‹

Providers are an abstraction between a flag management system and the OpenFeature SDK. Look here for a complete list of available providers. If the provider you're looking for hasn't been created yet, see the develop a provider section to learn how to build it yourself.

Once you've added a provider as a dependency, it can be registered with OpenFeature like this:

await OpenFeatureAPI.shared.setProviderAndWait(provider: MyProvider())

Asynchronous API that doesn't wait is also available

Targetingโ€‹

Sometimes, the value of a flag must consider some dynamic criteria about the application or user, such as the user's location, IP, email address, or the server's location. In OpenFeature, we refer to this as targeting. If the flag management system you're using supports targeting, you can provide the input data using the evaluation context.

// Configure your evaluation context and pass it to OpenFeatureAPI
let ctx = ImmutableContext(
targetingKey: userId,
structure: ImmutableStructure(attributes: ["product": Value.string(productId)]))
OpenFeatureAPI.shared.setEvaluationContext(evaluationContext: ctx)

Hooksโ€‹

Hooks allow for custom logic to be added at well-defined points of the flag evaluation life-cycle. Look here for a complete list of available hooks. If the hook you're looking for hasn't been created yet, see the develop a hook section to learn how to build it yourself.

Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level.

// add a hook globally, to run on all evaluations
OpenFeatureAPI.shared.addHooks(hooks: ExampleHook())

// add a hook on this client, to run on all evaluations made by this client
let client = OpenFeatureAPI.shared.getClient()
client.addHooks(ExampleHook())

// add a hook for this evaluation only
_ = client.getValue(
key: "key",
defaultValue: false,
options: FlagEvaluationOptions(hooks: [ExampleHook()]))

Trackingโ€‹

The tracking API allows you to use OpenFeature abstractions and objects to associate user actions with feature flag evaluations. This is essential for robust experimentation powered by feature flags. For example, a flag enhancing the appearance of a UI component might drive user engagement to a new feature; to test this hypothesis, telemetry collected by a hook or provider can be associated with telemetry reported in the client's track function.

let client = OpenFeatureAPI.shared.getClient()

// Track an event (uses the stored evaluation context on the client)
client.track(key: "test")

// Track an event with a numeric value
client.track(key: "test-value", details: ImmutableTrackingEventDetails(value: 5))

Set evaluation context via provider initialization or OpenFeatureAPI.shared.setEvaluationContext(...) before tracking. This client SDK follows the OpenFeature static-context tracking API and does not accept evaluation context at track invocation time.

Note that some providers may not support tracking; check the documentation for your provider for more information.

Loggingโ€‹

The SDK does not depend on any logging framework. It logs through the small OpenFeatureLogger protocol, so you can plug in whatever logger your app already uses:

public protocol OpenFeatureLogger {
func debug(_ message: @autoclosure () -> String)
func info(_ message: @autoclosure () -> String)
func warning(_ message: @autoclosure () -> String)
func error(_ message: @autoclosure () -> String)
}

Messages are autoclosures, so string interpolation only runs if your implementation emits the message. Implementations may be called from any thread and must be thread-safe.

Bring your own loggerโ€‹

Conform your logger to OpenFeatureLogger. For example, with Apple's unified logging:

import OpenFeature
import os

struct OSLogOpenFeatureLogger: OpenFeatureLogger {
private let logger = os.Logger(subsystem: "com.example.app", category: "openfeature")

func debug(_ message: @autoclosure () -> String) { logger.debug("\(message())") }
func info(_ message: @autoclosure () -> String) { logger.info("\(message())") }
func warning(_ message: @autoclosure () -> String) { logger.warning("\(message())") }
func error(_ message: @autoclosure () -> String) { logger.error("\(message())") }
}

Using swift-logโ€‹

If you use swift-log, the optional OpenFeatureSwiftLog product ships a ready-made SwiftLogLogger that forwards to a swift-log Logger:

import Logging
import OpenFeature
import OpenFeatureSwiftLog

let logger = SwiftLogLogger(Logger(label: "com.example.app.openfeature"))
// or simply: SwiftLogLogger(label: "com.example.app.openfeature")
OpenFeatureAPI.shared.setLogger(logger)

The core OpenFeature product never links swift-log; only apps that add OpenFeatureSwiftLog do.

Configure Loggerโ€‹

You can configure logging at three levels, with each level taking precedence over the previous:

1. Global (API-level) - affects all flag evaluations:

OpenFeatureAPI.shared.setLogger(OSLogOpenFeatureLogger())

2. Client-level - affects all evaluations from a specific client:

let client = OpenFeatureAPI.shared.getClient()
client.setLogger(OSLogOpenFeatureLogger())

3. Evaluation-level - affects a single flag evaluation:

let options = FlagEvaluationOptions(logger: OSLogOpenFeatureLogger())
let value = client.getBooleanValue(key: "my-flag", defaultValue: false, options: options)

Provider Supportโ€‹

Providers can optionally use the logger for debugging and diagnostics. The resolved OpenFeatureLogger is passed to providers during flag evaluation through the logger: parameter of the evaluation methods, allowing them to log relevant information without depending on any logging framework.

If no logger is configured, logging is disabled. The logger is completely optional for both SDK users and provider authors.

Migrating providers from swift-log (0.6.x)โ€‹

In 0.6.x the logger-aware FeatureProvider methods took a swift-log Logger?. They now take (any OpenFeatureLogger)?. A provider that still declares the old signature will compile, because the method simply stops matching the protocol requirement, but the SDK will call the default implementation instead and the provider's logging silently stops. Update all five logger-aware methods:

// Before (0.6.x)
func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?, logger: Logger?) throws
-> ProviderEvaluation<Bool>

// After
func getBooleanEvaluation(
key: String, defaultValue: Bool, context: EvaluationContext?, logger: (any OpenFeatureLogger)?
) throws -> ProviderEvaluation<Bool>

Apply the same change to getStringEvaluation, getIntegerEvaluation, getDoubleEvaluation and getObjectEvaluation. Inside the method, logger?.debug(...), logger?.info(...), logger?.warning(...) and logger?.error(...) calls that pass only a message keep working unchanged. OpenFeatureLogger takes just a message, so swift-log-specific arguments such as metadata: or source: no longer compile; fold that information into the message, and replace trace, notice or critical calls with the nearest of the four levels. Providers that only implement the plain evaluation methods (without logger:) need no changes. Drop the import Logging if the provider no longer uses swift-log directly.

Domainsโ€‹

Domains allow you to logically bind clients with providers, enabling the use of multiple providers within a single application. Each domain can have its own provider, and clients can be associated with a specific domain.

Support for domains is not yet available in the iOS SDK.

MultiProviderโ€‹

The MultiProvider allows you to combine multiple feature flag providers into a single provider, enabling you to use different providers for different flags or implement fallback mechanisms. This is useful when migrating between providers, implementing A/B testing across providers, or ensuring high availability.

Basic Usageโ€‹

import OpenFeature

Task {
// Create individual providers
let primaryProvider = PrimaryProvider()
let fallbackProvider = FallbackProvider()

// Create a MultiProvider with default FirstMatchStrategy
let multiProvider = MultiProvider(providers: [primaryProvider, fallbackProvider])

// Set the MultiProvider as the global provider
await OpenFeatureAPI.shared.setProviderAndWait(provider: multiProvider)

// Use flags normally - the MultiProvider will handle provider selection
let client = OpenFeatureAPI.shared.getClient()
let flagValue = client.getBooleanValue(key: "my-flag", defaultValue: false)
}

Evaluation Strategiesโ€‹

The MultiProvider supports different strategies for evaluating flags across multiple providers:

FirstMatchStrategy (Default)โ€‹

The FirstMatchStrategy evaluates providers in order and returns the first result that doesn't indicate "flag not found". If a provider returns an error other than "flag not found", that error is returned immediately.

let multiProvider = MultiProvider(
providers: [primaryProvider, fallbackProvider],
strategy: FirstMatchStrategy()
)
FirstSuccessfulStrategyโ€‹

The FirstSuccessfulStrategy evaluates providers in order and returns the first successful result (no error). Unlike FirstMatchStrategy, it continues to the next provider if any error occurs, including "flag not found".

let multiProvider = MultiProvider(
providers: [primaryProvider, fallbackProvider],
strategy: FirstSuccessfulStrategy()
)

Use Casesโ€‹

Provider Migration:

// Gradually migrate from OldProvider to NewProvider
let multiProvider = MultiProvider(providers: [
NewProvider(), // Check new provider first
OldProvider() // Fall back to old provider
])

High Availability:

// Use multiple providers for redundancy
let multiProvider = MultiProvider(providers: [
RemoteProvider(),
LocalCacheProvider(),
StaticProvider()
])

Environment-Specific Providers:

// Different providers for different environments
let providers = [
EnvironmentProvider(environment: "production"),
DefaultProvider()
]
let multiProvider = MultiProvider(providers: providers)

InMemoryProviderโ€‹

InMemoryProvider resolves flags from a configuration you supply, with no network calls and no mocking. It is useful for demos, local development, and for testing hooks, providers, or application code that consumes flags.

let provider = InMemoryProvider(flags: [
"boolean-flag": InMemoryFlag(
variants: ["on": .boolean(true), "off": .boolean(false)],
defaultVariant: "on"),
"greeting": InMemoryFlag(
variants: ["formal": .string("Good evening"), "casual": .string("hi")],
defaultVariant: "formal",
flagMetadata: ["version": .string("1.0.2")]),
])

await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)
OpenFeatureAPI.shared.getClient().getBooleanValue(key: "boolean-flag", defaultValue: false) // true

Targeting is expressed as a callback returning the key of the variant to resolve, or nil to fall back to the flag's defaultVariant:

InMemoryFlag(
variants: ["internal": .string("INTERNAL"), "external": .string("EXTERNAL")],
defaultVariant: "external",
contextEvaluator: { _, context in
context?.getValue(key: "customer") == .boolean(false) ? "internal" : nil
})

A resolution reports TARGETING_MATCH when the callback selects a variant, DEFAULT when it returns nil, STATIC when the flag has no callback, and DISABLED for a flag constructed with disabled: true. An unknown flag key resolves as FLAG_NOT_FOUND, and a variant whose value is not of the requested type as TYPE_MISMATCH.

The configuration can be changed after the provider is registered, which emits PROVIDER_CONFIGURATION_CHANGED:

provider.updateFlag(key: "boolean-flag", flag: InMemoryFlag(
variants: ["on": .boolean(true), "off": .boolean(false)],
defaultVariant: "off"))

provider.removeFlag(key: "greeting")

// Replaces the whole configuration; the event reports the union of the old and new flag keys.
provider.putConfiguration(["only-flag": InMemoryFlag(variants: ["on": .boolean(true)], defaultVariant: "on")])

Eventingโ€‹

Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. Initialization events (PROVIDER_READY on success, PROVIDER_ERROR on failure) are dispatched for every provider. Some providers support additional events, such as PROVIDER_CONFIGURATION_CHANGED.

Please refer to the documentation of the provider you're using to see what events are supported.

let cancellable = OpenFeatureAPI.shared.observe().sink { event in
switch event {
case ProviderEvent.ready:
// ...
default:
// ...
}
}

Shutdownโ€‹

A shutdown function is not yet available in the iOS SDK.

Extendingโ€‹

Develop a providerโ€‹

To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. You'll then need to write the provider by implementing the FeatureProvider interface exported by the OpenFeature SDK.

Status ownershipโ€‹

Providers are fully responsible for managing their own status. The SDK reads status from the provider but never sets it. You must keep status consistent with the events you emit, and the property must be thread-safe (it can be read concurrently from flag evaluation paths).

The easiest way to satisfy these requirements is to delegate to ProviderStatusTracker.

Example implementationโ€‹

import Combine
import OpenFeature

final class CustomProvider: FeatureProvider {
var hooks: [any Hook] = []
var metadata: ProviderMetadata = CustomMetadata()

// ProviderStatusTracker keeps `status` in sync with emitted events,
// handles thread safety, and replays the current status to new subscribers.
private let statusTracker = ProviderStatusTracker()
var status: ProviderStatus { statusTracker.status }
func observe() -> AnyPublisher<ProviderEvent, Never> { statusTracker.observe() }

func initialize(initialContext: EvaluationContext?) -> Future<Void, Never> {
Future { promise in
// Perform context-aware initialisation, then emit any non-.notReady status.
// .ready and .error are the most common outcomes.
self.statusTracker.send(.ready(nil))
promise(.success(()))
}
}

func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) -> Future<Void, Never> {
// Note: this may be called again before a previous lifecycle Future has
// resolved. Cancel any in-flight async work when a new call arrives.
Future { promise in
self.statusTracker.send(.reconciling(nil))
// ... re-initialize with new context ...
self.statusTracker.send(.contextChanged(nil)) // or .error(nil) on failure
promise(.success(()))
}
}

func getBooleanEvaluation(
key: String,
defaultValue: Bool,
context: EvaluationContext?
) throws -> ProviderEvaluation<Bool> {
// resolve a boolean flag value
}

...
}

Built a new provider? Let us know so we can add it to the docs!

Develop a hookโ€‹

To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. Implement your own hook by conforming to the Hook interface. To satisfy the interface, all methods (Before/After/Finally/Error) need to be defined.

class BooleanHook: Hook {
typealias HookValue = Bool

func before<HookValue>(ctx: HookContext<HookValue>, hints: [String: Any]) {
// do something
}

func after<HookValue>(ctx: HookContext<HookValue>, details: FlagEvaluationDetails<HookValue>, hints: [String: Any]) {
// do something
}

func error<HookValue>(ctx: HookContext<HookValue>, error: Error, hints: [String: Any]) {
// do something
}

func finally<HookValue>(ctx: HookContext<HookValue>, details: FlagEvaluationDetails<HookValue>, hints: [String: Any]) {
// do something
}
}

Built a new hook? Let us know so we can add it to the docs!