SDKiOS

Threading model (iOS)

How the Extentos iOS SDK concurrency works — ExtentosGlasses is Sendable, every sub-client call is async, MainActor guidance for UI, and why the package builds in Swift 5 language mode.

The concurrency contract below is verified against the current iOS source. Package install: Install.

Sendable instance, async calls

ExtentosGlasses is Sendable — you can hold one instance and call it from any task or actor without a data race. There's no requirement to confine it to a particular thread. Create it once (see Initialization) and share it.

Every capability call is async. Reach a primitive through its sub-client and await it — lifecycle and capture calls return an ExtentosResult to pattern-match (or discard explicitly):

let result = await glasses.camera.capturePhoto()  // ExtentosResult<Photo, CaptureError>
if case .success(let photo) = result { /* use photo */ }
_ = await glasses.audio.speak("Done.")            // ExtentosResult<Void, AudioError>

handleUrl(_:) and Extentos.requestSpeechRecognitionAuthorization() are likewise async. The SDK runs its work off the main thread; you don't manually hop queues to call it.

MainActor and UI

Because the SDK is Sendable and its methods are async, the idiomatic pattern is to call it from a Task and apply results to your UI on the MainActor:

@MainActor
final class CaptureViewModel: ObservableObject {
    @Published var lastPhoto: Photo?
    let glasses: ExtentosGlasses

    func capture() {
        Task {
            // capturePhoto() returns ExtentosResult<Photo, CaptureError> — pattern-match it, don't assign directly
            if case .success(let photo) = await glasses.camera.capturePhoto() {
                // back on the MainActor — safe to mutate @Published state
                self.lastPhoto = photo
            }
        }
    }
}

SwiftUI view modifiers like .onOpenURL already run on the main actor; spawning a Task { await glasses.handleUrl(url) } inside one (as in Initialization) moves the SDK work off the main thread while keeping the modifier's contract.

Streams and cancellation

Continuous primitives (transcription, frame streams, connection-state updates) are surfaced as async sequences. Iterate them inside a Task and cancel by cancelling that task — structured concurrency tears the iteration down. Store long-lived Task handles on your view model and cancel them in deinit or when the view disappears, so a stream doesn't outlive the screen that started it.

Swift 6 toolchain, Swift 5 language mode

The package declares swift-tools-version: 6.0, so it builds with a Swift 6 toolchain — but it compiles in the Swift 5 language mode (.swiftLanguageMode(.v5) on the GlassesCore target). The reason: the uniffi-generated Swift bindings the core ships use a global var that Swift 6's strict concurrency checking rejects. Until those bindings are isolated or regenerated clean, the language mode stays at 5.

What this means for you:

  • "Swift 6" unqualified overstates it — say Swift 6 toolchain, Swift 5 language mode.
  • The SDK's public API is unaffected — the Sendable / async contract above holds either way.
  • Your own app can adopt Swift 6 strict concurrency independently; the language-mode setting is scoped to the SDK's own target, not yours.

This is a tracked SDK-internal follow-up, not something you need to work around.

See Android threading for the Kotlin-coroutines counterpart.