Code Examples

Complete async/await examples (with completion-handler equivalents) for every SDK method

Complete, copy-pasteable examples for every public method of the Convert iOS SDK. All examples use Swift's async/await; completion-handler equivalents are shown where the SDK provides them. All examples assume a single ConvertSwiftSDK instance created at app startup (see Initialization).

Create the SDK

import ConvertSwiftSDK

// Key path: fetches config from the CDN.
let sdk = ConvertSwiftSDK(configuration: ConvertConfiguration(sdkKey: "YOUR_SDK_KEY"))

// Key path with options.
let sdk = ConvertSwiftSDK(configuration: ConvertConfiguration(
    sdkKey: "YOUR_SDK_KEY",
    environment: "prod",
    logLevel: .info
))

// Direct-data path: pre-fetched config bytes bypass the CDN.
let sdk = ConvertSwiftSDK(configData: preloadedData)

Wait for readiness

async/await

try await sdk.ready()
// Decisions are available from here on.

Completion handler (delivered on MainActor)

sdk.ready { result in
    // Called on the MainActor.
    switch result {
    case .success:
        // SDK is ready.
        break
    case .failure(let error):
        print(error.errorDescription ?? "unknown")
    }
}

ready() resolves on a successful or degraded load; it throws ConvertError only on an unrecoverable configuration error (empty SDK key, or empty/invalid direct-data). A transient network failure does NOT throw — the SDK resolves degraded.

Create a visitor context

// Anonymous visitor — auto-generated UUID, persisted in Keychain.
let context = sdk.createContext()

// Explicit visitor ID.
let context = sdk.createContext(visitorId: "visitor-42")

// Explicit visitor ID with initial attributes.
let context = sdk.createContext(
    visitorId: "visitor-42",
    attributes: ["plan": "premium", "accountAgeDays": 120]
)

// With location properties — required to activate a location-targeted experience.
let context = sdk.createContext(
    visitorId: "visitor-42",
    attributes: ["plan": "premium"],
    locationProperties: ["location": "pricing"]
)

createContext is synchronous and never blocks; it can be called before ready() resolves. Values in both attributes and locationProperties must be scalars (String, Int, Double, Bool) — nested dictionaries, arrays, and NSNull are dropped at call time and logged at debug. attributes feeds the audience gate; locationProperties feeds the location gate, and an experience that targets locations does not bucket without it. Read them back with context.attributes and context.locationProperties.

Run a single experience

if let variation = await context.runExperience("homepage-redesign") {
    switch variation.key {
    case "control":   renderControl()
    case "treatment": renderTreatment()
    default:          renderControl()
    }
} else {
    renderControl() // SDK not ready, visitor not bucketed, or experience absent
}

Run all experiences for the visitor

let variations = await context.runExperiences()
for variation in variations {
    applyVariation(variation)
}

runExperiences() returns only the experiences the visitor is bucketed into; experiences that fail audience or location rules are excluded. Returns [] when the SDK is not yet ready.

Completion handler

context.runExperiences { variations in
    // Called on the MainActor.
    for variation in variations {
        applyVariation(variation)
    }
}

Suppress the tracking event for a run call

// Evaluate but do not report to the network (e.g. consent-denied flow).
let variation = await context.runExperience("homepage-redesign", enableTracking: false)
let variations = await context.runExperiences(enableTracking: false)

Bucketing, sticky persistence, audience rules, and the internal bucketing event still fire — only the outbound network event is skipped. See Tracking Control.

Force a variation from a preview link

A Convert preview link carries convert_preview={experienceId}.{variationId}. On iOS it arrives as a deep link (custom URL scheme, universal link, or a scanned QR code). Parse the value with PreviewParam.parse(_:) and hand the pair to setPreview before the screen renders:

import ConvertSwiftSDK

// SwiftUI: .onOpenURL is synchronous, so hop into a Task for the async setPreview.
.onOpenURL { url in
    guard let raw = URLComponents(url: url, resolvingAgainstBaseURL: false)?
            .queryItems?.first(where: { $0.name == "convert_preview" })?.value,
          let parsed = PreviewParam.parse(raw) else { return }   // nil = not a preview link
    Task {
        await context.setPreview(
            experienceId: parsed.experienceId,
            variationId: parsed.variationId
        )
        await render()   // render after setPreview resolves
    }
}

setPreview resolves eagerly — it suspends until the experience is found, so the next runExperience sees the result immediately. If the experience is not in the config the SDK already holds, it fetches just that one experience on demand, so a preview link works on a normal production build against a draft or paused experiment, with no debug token.

// Forced — skips audiences, locations, environment, status, bucketing, and any stored decision.
let variation = await context.runExperience("pricing-test")

Two behaviors to design around:

  • Zero trace, context-wide. While a preview target is set on a context, that context records nothing — no bucketing events, no conversions, no segments — for every experience it runs, not just the forced one. Sibling experiences still evaluate and render normally.
  • Inert on bad input. An unresolvable experience or variation ID logs a [WARN] and clears any previously set target, so the context returns to fully normal behavior.

PreviewParam.parse returns nil unless the value is exactly two non-empty, all-digit components separated by a single . — so the guard above falls straight through for any other link. Preview state is per context: one context's target never leaks into another. See QA & Preview.

Evaluate a feature flag

let feature = await context.runFeature("checkout-v2")
if feature.status == .enabled {
    enableCheckoutV2()
}

runFeature is non-optional by contract — it returns Feature.disabled(key:) (not nil) when the SDK is not ready or the feature is absent. It never throws. Unlike the experience API, runFeature has no enableTracking parameter (Android parity).

Read feature variables

let feature = await context.runFeature("checkout-v2")

// Typed accessor — returns nil on key miss or type mismatch.
let color = feature.variable("ctaColor", as: String.self) ?? "#0066ff"
let limit = feature.variable("maxItems", as: Int.self) ?? 20
let price = feature.variable("price", as: Double.self) ?? 9.99
let experimental = feature.variable("experimental", as: Bool.self) ?? false

For JSON variables, read as Data and decode at the call site:

if let raw = feature.variable("config", as: Data.self) {
    let decoded = try? JSONDecoder().decode(MyConfig.self, from: raw)
}

Run all features

let features = await context.runFeatures()
for feature in features where feature.status == .enabled {
    print("enabled:", feature.key)
}

Returns one Feature per entry in the config, in config order. Returns [] when the SDK is not yet ready. No enableTracking parameter (Android parity).

Track a conversion

// Bare goal — no transactional data.
await context.trackConversion("signup-completed")

// With goal data.
let data: GoalData = [
    .amount:        .double(49.99),
    .productsCount: .double(3),
    .transactionId: .string("tx-42"),
]
await context.trackConversion("purchase-completed", goalData: data)

GoalData is [GoalDataKey: GoalDataValue] — use a dictionary literal. Never use a constructor form. trackConversion is async but never throws (AOD-6).

Force a repeat transaction (bypass dedup)

By default the same goal is recorded only once per visitor. For renewals or repeat purchases, bypass dedup with forceMultipleTransactions:

await context.trackConversion(
    "purchase-completed",
    goalData: [.amount: .double(49.99)],
    forceMultipleTransactions: true
)

On the repeat call the conversion event is skipped (already recorded), the transaction payload is sent, and the conversion event fires. See Tracking Conversions.

Set default segments (targeting input + event payload)

// Six recognised string keys; unknown keys are WARN-logged and ignored.
await context.setDefaultSegments([
    "country":     "US",
    "visitorType": "returning",
    "browser":     "Chrome",
])

Segment fields flow into outbound tracking events and feed audience rules. Each call merges over the visitor's existing segments (overlay semantics — existing recognised keys are overwritten, unset keys are untouched). Fires SystemEvent.segments once.

Set custom segment identifiers

await context.setCustomSegments(["vip", "beta-tester"])

Appends identifiers to the visitor's customSegments list. Backend owns dedup (matching JS SDK behavior). Fires SystemEvent.segments once.

Subscribe to and unsubscribe from events

let token = await sdk.on(.conversion) { payload in
    // Called each time a conversion is tracked.
    print("conversion fired:", payload)
}

// Cancel the subscription.
await sdk.off(token)

Pass any SystemEvent case to on(_:callback:). The callback is @Sendable and may be called from any concurrency context — dispatch to MainActor if you need to update the UI:

let token = await sdk.on(.ready) { _ in
    Task { @MainActor in updateUI() }
}

Toggle tracking at runtime

// Suppress — e.g. on consent withdrawal.
await sdk.setTrackingEnabled(false)

// Re-enable — e.g. on consent re-grant.
await sdk.setTrackingEnabled(true)

// Read the current state.
let isOn = await sdk.isTrackingEnabled()

Completion-handler overloads (delivered on MainActor):

sdk.setTrackingEnabled(false) { /* gate is closed */ }

sdk.isTrackingEnabled { isOn in
    print("tracking:", isOn)
}

When tracking is disabled, no events reach the network, but bucketing and sticky assignment are unaffected. See Tracking Control for the full three-layer consent model.

Background session wiring (optional)

Forward the AppDelegate background-session callback to the SDK for prompt OS release. This is optional — the SDK recovers persisted events on the next launch regardless.

// In your UIApplicationDelegate:
func application(
    _ application: UIApplication,
    handleEventsForBackgroundURLSession identifier: String,
    completionHandler: @escaping () -> Void
) {
    ConvertSwiftSDK.shared?.handleEventsForBackgroundURLSession(
        identifier: identifier,
        completionHandler: completionHandler
    )
}

The SDK rejects any identifier it does not own, so passing unrelated session identifiers is safe.

ConvertSwiftSDK.shared is a developer-assigned convenience, nil until you set it — the initializer does not install it and the SDK is not a singleton. Assign it once at startup (ConvertSwiftSDK.shared = sdk) if you want to reach the instance from an AppDelegate callback like the one above, or hold your own reference instead.

Error handling

do {
    try await sdk.ready()
} catch ConvertError.invalidSdkKey(let detail) {
    print("Bad SDK key:", detail)
} catch ConvertError.invalidConfiguration(let detail) {
    print("Bad config:", detail)
} catch {
    print("Unexpected:", error)
}

Only ready() throws. All decisioning and tracking methods degrade silently (return nil, [], or a disabled Feature; log at warn).

Next steps


Did this page help you?