SDKs
Swift SDK
Featurevisor Swift SDK can be used in Apple devices targeting several operating systems including: iOS, iPadOS, macOS, tvOS, and watchOS.
Installation#
In your Swift application, add this package using Swift Package Manager:
.package(url: "https://github.com/featurevisor/featurevisor-swift2.git", from: "2.0.0")Then add the product dependency:
.product(name: "Featurevisor", package: "featurevisor-swift2")Public API#
The main runtime API is createFeaturevisor():
let f: Featurevisor = createFeaturevisor( FeaturevisorOptions(datafile: datafileContent))Most applications only need createFeaturevisor, Featurevisor, and FeaturevisorOptions. Public extension and observability types include FeaturevisorModule, FeaturevisorDiagnostic, and the datafile model types.
The SDK supports iOS 14, macOS 11, tvOS 14, and watchOS 7 or newer. Shared Featurevisor, child instance, and OpenFeature provider state is safe to use from concurrent callers. Module, diagnostic, event, and tracking callbacks are @Sendable; callback implementations must synchronize any mutable state they capture.
Initialization#
The SDK can be initialized by passing datafile content directly:
import Foundationimport Featurevisorlet datafileURL = URL(string: "https://cdn.yoursite.com/datafile.json")!let data = try Data(contentsOf: datafileURL)let datafileContent = try DatafileContent.fromData(data)let f = createFeaturevisor( FeaturevisorOptions( datafile: datafileContent ))Evaluation types#
We can evaluate 3 types of values against a particular feature:
- Flag (
Bool): whether the feature is enabled or not - Variation (
String): the variation of the feature (if any) - Variables: variable values of the feature (if any)
These evaluations are run against the provided context.
Context#
Contexts are attribute values that we pass to SDK for evaluating features against.
Think of the conditions that you define in your segments, which are used in your feature's rules.
They are plain dictionaries:
let context: Context = [ "userId": .string("123"), "country": .string("nl"),]Setting initial context#
You can set context at the time of initialization:
let f = createFeaturevisor( FeaturevisorOptions( context: [ "deviceId": .string("123"), "country": .string("nl"), ] ))Setting after initialization#
You can also set more context after the SDK has been initialized:
f.setContext([ "userId": .string("234"),])This will merge the new context with the existing one (if already set).
Replacing existing context#
If you wish to fully replace the existing context, you can pass true in second argument:
f.setContext( [ "deviceId": .string("123"), "userId": .string("234"), "country": .string("nl"), "browser": .string("chrome"), ], replace: true)Manually passing context#
You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations:
let context: Context = [ "userId": .string("123"), "country": .string("nl"),]let isEnabled = f.isEnabled("my_feature", context)let variation = f.getVariation("my_feature", context)let variableValue = f.getVariable("my_feature", "my_variable", context)When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.
Check if enabled#
Once the SDK is initialized, you can check if a feature is enabled or not:
let featureKey = "my_feature"let isEnabled = f.isEnabled(featureKey)if isEnabled { // do something}You can also pass additional context per evaluation:
let isEnabled = f.isEnabled(featureKey, [ // ...additional context])Getting variation#
If your feature has any variations defined, you can evaluate them as follows:
let featureKey = "my_feature"let variation = f.getVariation(featureKey)if variation == "treatment" { // do something for treatment variation} else { // handle default/control variation}Additional context per evaluation can also be passed:
let variation = f.getVariation(featureKey, [ // ...additional context])Getting variables#
Your features may also include variables, which can be evaluated as follows:
let variableKey = "bgColor"let bgColorValue = f.getVariable("my_feature", variableKey)Additional context per evaluation can also be passed:
let bgColorValue = f.getVariable("my_feature", variableKey, [ // ...additional context])Type specific methods#
Next to generic getVariable() methods, there are also type specific methods available for convenience:
f.getVariableBoolean(featureKey, variableKey, context)f.getVariableString(featureKey, variableKey, context)f.getVariableInteger(featureKey, variableKey, context)f.getVariableDouble(featureKey, variableKey, context)f.getVariableArray(featureKey, variableKey, context)f.getVariableObject(featureKey, variableKey, context)f.getVariableJSON(featureKey, variableKey, context)Type specific methods do not coerce strings or booleans into numbers. They return nil when the value does not match the requested type.
Getting all evaluations#
You can get evaluations of all features available in the SDK instance:
let allEvaluations = f.getAllEvaluations([:])print(allEvaluations)This is handy especially when you want to pass all evaluations from a backend application to the frontend.
Sticky#
For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched datafile:
Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use SpawnOptions(sticky: ...) when a child needs its own sticky state.
Initialize with sticky#
let f = createFeaturevisor( FeaturevisorOptions( sticky: [ "myFeatureKey": EvaluatedFeature( enabled: true, variation: "treatment", variables: ["myVariableKey": .string("myVariableValue")] ), "anotherFeatureKey": EvaluatedFeature(enabled: false), ] ))Set sticky afterwards#
f.setSticky([ "myFeatureKey": EvaluatedFeature( enabled: true, variation: "treatment", variables: ["myVariableKey": .string("myVariableValue")] ), "anotherFeatureKey": EvaluatedFeature(enabled: false),], replace: true)Setting datafile#
You may also initialize the SDK without passing datafile, and set it later on:
f.setDatafile(datafileContent)You can also set using raw JSON string:
f.setDatafile(json: jsonString)Merging by default#
By default, setDatafile merges the incoming datafile with the SDK instance's existing datafile:
- incoming
featuresandsegmentsoverride matching keys - existing
featuresandsegmentsthat are missing from the incoming datafile are kept revision,schemaVersion, andfeaturevisorVersionare taken from the incoming datafile
This means you can call setDatafile more than once with different datafiles, and the SDK instance accumulates their features and segments together.
Replacing#
Pass replace: true to replace the stored datafile entirely:
f.setDatafile(datafileContent, replace: true)f.setDatafile(json: jsonString, replace: true)Loading datafiles on demand#
Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront.
This pairs well with targets, where each target produces a smaller datafile for a specific part of your application:
let f = createFeaturevisor(FeaturevisorOptions())func loadDatafile(target: String) { let url = URL(string: "https://cdn.yoursite.com/production/featurevisor-\(target).json")! if let data = try? Data(contentsOf: url), let datafile = try? DatafileContent.fromData(data) { // merges into whatever was loaded before f.setDatafile(datafile) }}loadDatafile(target: "products")// later, when the user reaches checkoutloadDatafile(target: "checkout")Updating datafile#
You can set the datafile as many times as you want in your application, which will result in emitting a datafile_set event that you can listen and react to accordingly.
Interval-based update#
import Foundationlet interval: TimeInterval = 5 * 60Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in if let data = try? Data(contentsOf: datafileURL), let datafile = try? DatafileContent.fromData(data) { f.setDatafile(datafile) }}Diagnostics#
By default, Featurevisor reports diagnostics to the console for info level and above with a [Featurevisor] prefix.
Levels#
Available diagnostic levels are fatal, error, warn, info, and debug.
Set the level during initialization or update it afterwards:
let f = createFeaturevisor( FeaturevisorOptions(logLevel: .debug))f.setLogLevel(.info)Handler#
Use onDiagnostic to send structured diagnostics to your observability system:
let f = createFeaturevisor( FeaturevisorOptions( logLevel: .info, onDiagnostic: { diagnostic in print(diagnostic.level, diagnostic.code, diagnostic.message) } ))Modules can also subscribe to diagnostics or report their own diagnostics from setup using the provided module API.
Every diagnostic has level, code, message, and an object-shaped details dictionary. Optional module, moduleName, and originalError fields describe provenance. Evaluation metadata belongs in details.
Events#
Featurevisor SDK implements a simple event emitter that allows you to listen to runtime events.
datafile_set#
let unsubscribe = f.on(.datafileSet) { payload in print(payload.params)}unsubscribe()context_set#
let unsubscribe = f.on(.contextSet) { _ in // handle context updates}unsubscribe()sticky_set#
let unsubscribe = f.on(.stickySet) { _ in // handle sticky updates}unsubscribe()error#
let unsubscribe = f.on(.error) { payload in if case .object(let diagnostic)? = payload.params["diagnostic"] { print(diagnostic["message"] ?? "") }}unsubscribe()The error event is emitted for diagnostics whose level is error.
Evaluation details#
If you need evaluation metadata, use:
let flagDetails = f.evaluateFlag("my_feature")let variationDetails = f.evaluateVariation("my_feature")let variableDetails = f.evaluateVariable("my_feature", "my_variable")Modules#
Modules allow you to intercept evaluation inputs and outputs.
Defining a module#
let module = FeaturevisorModule( name: "my-module", setup: { api in api.reportDiagnostic( FeaturevisorDiagnostic( level: .info, code: "module_ready", message: "Module is ready" ) ) }, before: { options in var updated = options updated.dependencies.context["someAdditionalAttribute"] = .string("value") return updated }, bucketKey: { options in options.bucketKey }, bucketValue: { options in options.bucketValue }, after: { evaluation, _ in evaluation }, close: { // clean up module resources })Registering modules#
let f = createFeaturevisor( FeaturevisorOptions( modules: [module] ))let removeModule = f.addModule(module)removeModule?()Child instance#
You can spawn child instances with inherited context:
let child = f.spawn([ "userId": .string("123"),])let enabled = child.isEnabled("my_feature")let flagEvaluation = child.evaluateFlag("my_feature")let variationEvaluation = child.evaluateVariation("my_feature")let variableEvaluation = child.evaluateVariable("my_feature", "my_variable")A child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. Calling close() removes both child-owned listeners and subscriptions delegated to the parent.
Close#
To clear listeners and close resources:
f.close()CLI usage#
The package also ships an executable named featurevisor.
All three commands accept repeatable --target=<target> options. test builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. benchmark and assess-distribution run independently against every selected Target datafile. Without --target, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
Test#
swift run featurevisor test \ --projectDirectoryPath=/path/to/featurevisor-projectBenchmark#
swift run featurevisor benchmark \ --projectDirectoryPath=/path/to/featurevisor-project \ --environment=production \ --feature=my_feature \ --context='{"userId":"123"}' \ --n=1000Assess distribution#
swift run featurevisor assess-distribution \ --projectDirectoryPath=/path/to/featurevisor-project \ --environment=production \ --feature=my_feature \ --populateUuid=userId \ --n=1000OpenFeature#
The package exposes FeaturevisorOpenFeature as a separate library product. Targets using it should declare both package dependencies:
.package(url: "https://github.com/featurevisor/featurevisor-swift2.git", from: "2.0.0"),.package(url: "https://github.com/open-feature/swift-sdk.git", from: "0.5.0")Then add the products needed by that target:
.product(name: "Featurevisor", package: "featurevisor-swift2"),.product(name: "FeaturevisorOpenFeature", package: "featurevisor-swift2"),.product(name: "OpenFeature", package: "swift-sdk")import Featurevisorimport FeaturevisorOpenFeatureimport OpenFeaturelet provider = FeaturevisorOpenFeatureProvider( options: FeaturevisorOptions(datafile: datafile))await OpenFeatureAPI.shared.setProviderAndWait( provider: provider, initialContext: ImmutableContext(targetingKey: "user-123"))let client = OpenFeatureAPI.shared.getClient()let enabled = client.getBooleanValue( key: "checkout", defaultValue: false)Use checkout for a flag, checkout:variation for its variation, and checkout:title for its title variable. Boolean variables use the boolean resolver. Lists, structures, and JSON variables use the object resolver.
The provider maps Featurevisor values to OpenFeature resolvers as follows:
| Key | Resolver |
|---|---|
feature | Boolean flag |
feature:variation | String variation |
feature:variable | Resolver matching the variable schema |
Integer variables can be resolved with either the integer or double resolver. Non-finite doubles are rejected with TYPE_MISMATCH. Invalid JSON variables also return TYPE_MISMATCH and the caller's default value.
OpenFeature's targeting key maps to userId by default. targetingKeyField, keySeparator, and variationKey can customize the mapping. Call provider.close() when the application owns the provider lifecycle and is finished with it.
You can initialize the provider from raw JSON when the application has not decoded a datafile yet:
let provider = FeaturevisorOpenFeatureProvider( options: FeaturevisorOptions(logLevel: .warn), datafileJSON: json)Malformed JSON returns the OpenFeature PARSE_ERROR code with Could not parse datafile. The provider keeps returning that error until a valid datafile is set through provider.featurevisor.setDatafile(...). Successful replacement clears the parse error.
You can also reuse an existing Featurevisor instance:
let featurevisor = createFeaturevisor(FeaturevisorOptions(datafile: datafile))let provider = FeaturevisorOpenFeatureProvider(featurevisor: featurevisor)The caller owns an instance passed this way. Closing the provider does not close it. Call featurevisor.close() when every consumer is finished with it. When the provider creates the instance from options, the provider owns and closes it. If both are supplied, featurevisor takes precedence over options.
Featurevisor evaluation reasons map to OpenFeature reasons:
| Featurevisor reason | OpenFeature reason |
|---|---|
required, forced, sticky, rule, variable overrides | TARGETING_MATCH |
allocated | SPLIT |
| disabled flag, variation, or variable | DISABLED |
| no match, out of range, variable default | DEFAULT |
| missing feature, variable, or variations | ERROR with FLAG_NOT_FOUND |
| evaluation error | ERROR with GENERAL |
Resolution metadata includes featureKey, featurevisorReason, schemaVersion, and revision. It also includes variableKey, ruleKey, bucketKey, bucketValue, forceIndex, and variableOverrideIndex when those values are available. Variation resolutions populate OpenFeature's variant field.
The provider forwards OpenFeature tracking calls to onTrack. Closing a provider it owns closes Featurevisor modules and subscriptions. Closing a provider created with a caller-owned Featurevisor instance only removes provider subscriptions.
The base Featurevisor product does not compile or link OpenFeature provider code into an application target. Swift Package Manager still resolves the package-level swift-sdk dependency because both products share one package manifest. Projects that require dependency-download isolation would need the provider in a separate repository.
See the OpenFeature provider guide for resolution reasons, errors, metadata, tracking, lifecycle, and providers for other languages.
GitHub repositories#
- See SDK repository here: featurevisor/featurevisor-swift2
- See example application repository here: featurevisor/featurevisor-example-swift

