Skip to content

ObjCInterop

Joseph Samir edited this page Jun 21, 2026 · 2 revisions

Objective-C / Completion-Handler Interop

The Convert iOS SDK is Swift-first and async/await-native. Objective-C callers and UIKit-style completion-handler call sites are fully supported through two extension files that provide callback overloads of every async method. This page shows the callback forms; the Swift async forms are in Code Examples.

There is no Java on Apple platforms — the iOS equivalent of cross-language interop is Objective-C, covered below.

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)

Objective-C callers use the same initializers — they are bridged automatically because ConvertSwiftSDK is @objc-compatible via its Sendable final class shape.

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> bridges cleanly to Objective-C as a two-parameter callback (success Bool + nullable error) when called from .m files.

Create a context and run experiences

createContext(visitorId:attributes:) is synchronous and non-throwing — call it directly:

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

runExperience and runExperiences have completion-handler overloads defined in ConvertContext+Completion.swift:

// 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 async Callback / 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

Clone this wiki locally