---
title: Threading model (iOS)
description: 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.
type: guide
platform: ios
related:
  - /docs/sdk/ios/initialization
  - /docs/sdk/ios/lifecycle
  - /docs/sdk/android/threading
---

> The concurrency contract below is verified against the current iOS source. Package install: [Install](/docs/sdk/ios/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](/docs/sdk/ios/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):

```swift
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`:

```swift
@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](/docs/sdk/ios/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](/docs/sdk/android/threading) for the Kotlin-coroutines counterpart.
