Objective-C Interop

Using the SDK from Objective-C / UIKit via completion-handler overloads

The Convert iOS SDK is Swift-first and async/await-native. UIKit-style callback call sites are served by completion-handler overloads on four methods — ready, setTrackingEnabled, isTrackingEnabled, and runExperiences. Everything else is async-only and is reached from a callback site with Task { }. This page shows the callback forms; the Swift async forms are in Code Examples.

The SDK's public surface is Swift-only — it exposes no @objc API. Nothing here is directly callable from a .m file. Objective-C call sites go through a small @objc Swift shim you write in your own app; the pattern is in Calling async SDK methods from Objective-C below. There is no Java on Apple platforms — the iOS equivalent of cross-language interop is Objective-C, covered that way.

Swift 6 and Sendable

The SDK is compiled with .swiftLanguageMode(.v6) — strict concurrency is on. ConvertSwiftSDK is a Sendable final class (all stored properties are immutable lets of Sendable types); ConvertContext is likewise Sendable with no @unchecked suppression. Both types cross actor boundaries safely.

Completion callbacks are delivered on @MainActor so UIKit updates can happen directly inside the closure without a DispatchQueue.main.async wrapper.

Initialize the SDK

The SDK uses plain initializers, not a builder chain:

import ConvertSwiftSDK

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

// With direct-data config (no network fetch)
let sdk = ConvertSwiftSDK(configData: myConfigData)

These initializers are not visible to Objective-C: ConvertSwiftSDK is a plain Swift final class that neither inherits NSObject nor carries @objc, so it is not emitted into the generated Objective-C header. Construct the SDK in Swift — either at your @main / AppDelegate entry point, or inside the @objc shim shown below.

Wait for readiness

The async ready() method has a completion-handler overload that delivers on @MainActor:

// Swift async
try await sdk.ready()

// Completion-handler (UIKit / Objective-C style)
sdk.ready { result in
    // called on @MainActor
    switch result {
    case .success:
        let context = sdk.createContext()
        // ...
    case .failure(let error):
        print("SDK error:", error)
    }
}

Result<Void, ConvertError> is a Swift-only type — it does not cross into Objective-C. To surface readiness in a .m file, re-shape it inside your @objc shim (the wrapper below maps it to a nullable NSError).

Create a context and run experiences

createContext(visitorId:attributes:locationProperties:) is synchronous and non-throwing — call it directly. All three parameters are optional:

let context = sdk.createContext()
let knownContext = sdk.createContext(visitorId: "user-42", attributes: ["plan": "pro"])

// Location-targeted experiences need the location gate's properties:
let onPricing = sdk.createContext(
    visitorId: "user-42",
    locationProperties: ["location": "pricing"]
)

runExperiences has a completion-handler overload (defined in ConvertContext+Completion.swift); the singular runExperience does not:

// Swift async
let variation = await context.runExperience("homepage-redesign")

// Completion-handler
context.runExperiences(enableTracking: true) { variations in
    // called on @MainActor
    for v in variations { applyVariation(v) }
}

Note: There is no completion-handler overload for the single runExperience(_:enableTracking:) call. ConvertContext+Completion.swift ships exactly one experience overload: runExperiences(enableTracking:completion:). For single-experience call sites in non-async contexts, use Task { let v = await context.runExperience("key") } or call runExperiences(completion:) and filter the result.

Run features

// Swift async
let feature = await context.runFeature("checkout-v2")
if feature.status == .enabled {
    // show the new checkout
}

// There is no completion-handler overload for runFeature or runFeatures —
// ConvertContext+Completion.swift does not include these overloads.
// From a non-async context, use Task:
Task {
    let feature = await context.runFeature("checkout-v2")
    await MainActor.run { applyFeature(feature) }
}

Track a conversion

trackConversion is async; call it from a Task for UIKit callers:

// Swift async
await context.trackConversion("purchase-goal")
await context.trackConversion(
    "purchase-goal",
    goalData: GoalData(amount: 49.99, transactionId: "tx-42"),
    forceMultipleTransactions: false
)

// From a UIKit action handler (non-async context)
Task {
    await context.trackConversion("purchase-goal")
}

Toggle tracking at runtime

The runtime tracking toggle has completion-handler overloads in ConvertSwiftSDK+Completion.swift:

// Swift async
await sdk.setTrackingEnabled(false)
let isOn = await sdk.isTrackingEnabled()

// Completion-handler forms (ConvertSwiftSDK+Completion.swift)
sdk.setTrackingEnabled(false) {
    // called on @MainActor once the actor flag is closed
}
sdk.isTrackingEnabled { isOn in
    print("tracking:", isOn)
}

Subscribe to events

The event bus (sdk.on(_:callback:) / sdk.off(_:)) is async:

// Swift async
let token = await sdk.on(.ready) { _ in
    print("SDK is ready")
}
// later:
await sdk.off(token)

// From a UIKit context (non-async)
Task {
    let token = await sdk.on(.bucketing) { payload in
        // payload is @Sendable — safe to use across actors
        print("bucketed:", payload)
    }
}

The callback closure is @escaping @Sendable — it may be called from any actor/thread, so keep it free of non-Sendable captures.

Calling async SDK methods from Objective-C (.m files)

Swift async methods are not directly callable from Objective-C. The recommended pattern for .m files is to write a Swift bridging wrapper that calls the SDK from Task { }:

// BridgingHelper.swift
import ConvertSwiftSDK

@objc class ConvertBridge: NSObject {
    private let sdk: ConvertSwiftSDK

    @objc init(sdkKey: String) {
        sdk = ConvertSwiftSDK(configuration: ConvertConfiguration(sdkKey: sdkKey))
    }

    @objc func ready(completion: @escaping (NSError?) -> Void) {
        sdk.ready { result in
            switch result {
            case .success: completion(nil)
            case .failure(let err): completion(err as NSError)
            }
        }
    }

    @objc func setTrackingEnabled(_ enabled: Bool, completion: @escaping () -> Void) {
        sdk.setTrackingEnabled(enabled, completion: completion)
    }
}

Use @objc on the wrapper class and any method you need to call from .m files.

Quick reference

Swift asyncCallback / UIKit pattern
ConvertSwiftSDK(configuration:)Same — plain initializer, no builder
try await sdk.ready()sdk.ready { result in … } (on @MainActor)
sdk.createContext()Same — synchronous, call directly
await context.runExperiences(enableTracking:)context.runExperiences(enableTracking:) { variations in … } (on @MainActor)
await context.trackConversion(_:goalData:)Task { await context.trackConversion(…) }
await sdk.setTrackingEnabled(_:)sdk.setTrackingEnabled(_:) { } (on @MainActor)
await sdk.isTrackingEnabled()sdk.isTrackingEnabled { isOn in … } (on @MainActor)
await sdk.on(_:callback:)Task { let token = await sdk.on(…) { … } }

Related pages


Did this page help you?