Initialization

ConvertSwiftSDK(configuration:), the ready() gate, direct-data mode, completion-handler overloads

ConvertSwiftSDK is constructed with a plain initializer — there is no builder pattern. Create exactly one instance per process — ideally from AppDelegate.application(_:didFinishLaunchingWithOptions:) or your SwiftUI @main entry point — because the SDK owns a long-lived URLSession, an offline event queue, a background-session manager, and a config-refresh scheduler. Multiple instances waste resources and can produce duplicate tracking events.

The initializer

import ConvertSwiftSDK

let sdk = ConvertSwiftSDK(configuration: ConvertConfiguration(sdkKey: "YOUR_SDK_KEY"))

ConvertSwiftSDK.init(configuration:) is non-throwing and non-blocking — it returns immediately. Config loading begins in a detached Task; validation and the live fetch surface through ready().

Every ConvertConfiguration field except sdkKey carries a JS-SDK-parity default. See Configuration Options for the full list.

Awaiting readiness — ready()

Config fetch is asynchronous. Until the first config loads, runExperience returns nil and runFeature returns a disabled Feature. Gate your first decision call with await sdk.ready():

// async/await (preferred)
do {
    try await sdk.ready()
    // Config is available — safe to call runExperience / runFeature.
} catch {
    // Only throws on an unrecoverable configuration error
    // (empty SDK key, or empty/invalid direct-data).
    // A transient network failure does NOT throw — the SDK resolves
    // degraded using a cached config if one exists.
}

ready() latches: once it resolves, subsequent calls return immediately. If a cache exists from a previous launch, ready() may resolve before the live fetch completes — the SDK is immediately usable offline.

Completion-handler overload

For UIKit-style call sites that prefer a callback, a completion-handler twin delivers the result on the MainActor:

sdk.ready { result in
    // Called on MainActor
    switch result {
    case .success:
        let ctx = self.sdk.createContext()
        let variation = // await ctx.runExperience(...)
    case .failure(let error):
        print("SDK configuration error: \(error)")
    }
}

Direct-data mode (pre-fetched config)

If you already have a config blob — shipped with your app bundle, fetched by a separate layer, or a test fixture — skip the network fetch entirely by using the configData initializer:

let configData: Data = loadBakedConfig() // your own loader
let sdk = ConvertSwiftSDK(configData: configData)
try await sdk.ready() // validates the payload; throws if empty or invalid

Resolution rules:

  • init(configData:) used → the SDK validates the data and skips the initial network fetch entirely.
  • init(configuration:) used (standard path) → the SDK loads from a disk cache first (if available), then refreshes from the live CDN.
  • Invalid or empty data / empty SDK keyready() throws ConvertError.invalidConfiguration(_:); every subsequent decision call degrades (returns nil / disabled Feature).

Subscribing to events

Beyond readiness, the SDK emits named events on its event bus. Subscribe with on(_:callback:), which returns an EventListenerToken:

import ConvertSwiftSDK

let token = await sdk.on(.bucketing) { payload in
    // payload carries the bucketing result (experienceKey, variationKey, visitorId)
}

// Later — unsubscribe with the token (preferred):
await sdk.off(token)

The SystemEvent cases are listed in Return Types & Models and explained in the shared Event System doc.

Creating a visitor context

A ConvertContext binds one visitor to the SDK. createContext(visitorId:attributes:locationProperties:) takes three optional parameters, so there are four shapes worth knowing:

// 1) Auto-visitor — UUID persisted in the Keychain, stable across launches
//    (best-effort stability across reinstalls; never IDFA/IDFV).
let ctx = sdk.createContext()

// 2) Explicit visitor — you own the ID (e.g. your login ID).
let ctx = sdk.createContext(visitorId: "visitor-42")

// 3) Explicit visitor + initial attributes (the AUDIENCE gate).
let ctx = sdk.createContext(
    visitorId: "visitor-42",
    attributes: ["plan": "premium", "accountAgeDays": 120]
)

// 4) With location properties (the LOCATION gate) — needed to activate a
//    location-targeted experience.
let ctx = sdk.createContext(
    visitorId: "visitor-42",
    attributes: ["plan": "premium"],
    locationProperties: ["location": "pricing"]
)

createContext is synchronous and non-blocking — a context can be created before ready() resolves (decisions will degrade until config is available). Passing an explicit visitorId neither reads nor writes the auto-UUID, so you can hold an anonymous context and a signed-in context in the same session.

Visitor ID persistence: the auto-generated UUID is written to the Keychain with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, so it persists across app launches but never syncs to the user's other devices via iCloud Keychain. Continuity across an uninstall and reinstall is best-effort only — the Keychain item may survive on some iOS versions and backup configurations, but this is not guaranteed. See Visitor Context.

attributes vs locationProperties

They are two separate maps feeding two separate gates, and mixing them up is the usual cause of a location-targeted experience never bucketing:

MapGate it feedsRead back as
attributesAudience rules — visitor properties (plan, country, …)ConvertContext.attributes
locationPropertiesLocation rules — where in your app the visitor isConvertContext.locationProperties

An experience with no locations configured is unrestricted and buckets regardless. An experience with locations matches only when the properties you supply satisfy one of those locations — so omitting locationProperties (or leaving it empty) means a location-targeted experience does not bucket. Both maps accept only scalar values (String, Int, Double, Bool); anything else is dropped at call time and logged at debug. See Rule Evaluation & Targeting.

SwiftUI entry point pattern

import SwiftUI
import ConvertSwiftSDK

@main
struct MyApp: App {
    let sdk = ConvertSwiftSDK(configuration: ConvertConfiguration(sdkKey: "YOUR_SDK_KEY"))

    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    try? await sdk.ready()
                }
                .environmentObject(sdk)  // share the single SDK instance however your app prefers
        }
    }
}

UIKit AppDelegate pattern

import UIKit
import ConvertSwiftSDK

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var sdk: ConvertSwiftSDK!

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        sdk = ConvertSwiftSDK(configuration: ConvertConfiguration(
            sdkKey: "YOUR_SDK_KEY",
            logLevel: .info
        ))
        sdk.ready { result in
            // result delivered on MainActor
            if case .failure(let error) = result {
                print("Convert SDK configuration error: \(error)")
            }
        }
        return true
    }

    // Forward background URLSession events to the SDK for durable delivery.
    func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping () -> Void
    ) {
        sdk.handleEventsForBackgroundURLSession(
            identifier: identifier,
            completionHandler: completionHandler
        )
    }
}

The handleEventsForBackgroundURLSession forwarding is optional — it lets the OS release the background relaunch sooner once the SDK's background URLSession events are acknowledged. Events are never lost if you omit it; only the prompt-wake-completion optimization is skipped.

Next steps


Did this page help you?