SpeechAnalyzer and SpeechTranscriber in iOS 26: On-Device Speech-to-Text in Swift

SpeechAnalyzer replaces SFSpeechRecognizer in iOS 26 with an actor-based, fully on-device API. Compose SpeechTranscriber, DictationTranscriber, and SpeechDetector modules, consume results from an AsyncSequence, and ship long-form Swift transcription.

SpeechAnalyzer iOS 26 Swift Guide (2026)

Updated: August 12, 2026

SpeechAnalyzer is Apple's actor-based, fully on-device speech-to-text framework introduced in iOS 26 (and iPadOS 26, macOS 26, Mac Catalyst 26, visionOS 26, and tvOS 26), and it replaces SFSpeechRecognizer for new work. You compose a session by attaching modules (SpeechTranscriber for long-form transcription, DictationTranscriber for short utterances, and SpeechDetector for voice activity) and consume results from an AsyncSequence. There is no server fallback, and no watchOS support in the current SDK.

  • SpeechAnalyzer ships in iOS 26, iPadOS 26, macOS 26, Mac Catalyst 26, visionOS 26, and tvOS 26. Notably, not watchOS 26.
  • The API is modular: attach SpeechTranscriber, DictationTranscriber, or SpeechDetector to a single SpeechAnalyzer actor.
  • It's on-device only, runs in a separate system process, and downloads locale models on demand through AssetInventory.
  • Results arrive as an AsyncSequence of segments. Each is marked volatile (may change) or final (locked in).
  • SpeechTranscriber wins accuracy and long-form benchmarks, but drops custom vocabulary and server fallback. Stay on SFSpeechRecognizer if you need either.
  • Live-mic capture still uses AVAudioEngine; convert to the analyzer's preferred format with AVAudioConverter on a background thread.

What is SpeechAnalyzer in iOS 26?

SpeechAnalyzer is a Swift actor that coordinates one or more transcription modules against a shared audio stream. Apple introduced it at WWDC25 as the modern replacement for the venerable SFSpeechRecognizer, and the design is a clean break. Everything runs on device, everything is async, and results are delivered as an AsyncSequence instead of a mutable delegate callback. The actual speech model lives outside your app's memory space, in a system process, so a 90-minute lecture won't push your app anywhere near the transcription memory ceiling that made the old SFSpeechRecognizer 60 seconds per request limit famous.

Because the analyzer is an actor, you interact with it from any concurrency domain without manual queues. The Speech framework you already imported for SFSpeechRecognizer now vends both APIs side by side. Old code keeps compiling; new code targets the new types. If you've already worked with async streams in Swift, the mental model in the Swift TaskGroup and AsyncStream guide translates directly: feed input, consume results, cancel when done.

SpeechAnalyzer vs SFSpeechRecognizer

The short version: SpeechAnalyzer is more accurate, handles long-form audio natively, ships a bigger model, and removes the request-length ceiling. It also drops custom vocabulary hints and the server-side fallback path. Third-party benchmarks published in mid-2026 put SpeechTranscriber ahead of Whisper Small on both clean and noisy LibriSpeech splits while running roughly 3x faster, and comfortably ahead of the older SFSpeechRecognizer engine on every axis. Honestly, compare the two before you migrate. The trade-offs are real.

CapabilitySpeechAnalyzer (iOS 26)SFSpeechRecognizer (iOS 10+)
IntroducediOS 26iOS 10
ConcurrencyActor + AsyncSequenceDelegate / completion callbacks
On-deviceAlways (no server path)Optional since iOS 13
Long-form audio (hours)YesNo, 60s per request limit
Language coverage~30 locales via SpeechTranscriberLegacy dictation locales
Custom vocabularyNot yet supportedYes
Server fallbackNoYes
Runs in your processNo, separate system processYes
watchOS supportNoYes

Two consequences worth internalizing. First, if your app pins a specific pharmaceutical, legal, or gaming vocabulary via SFSpeechRecognitionRequest.contextualStrings, you can't yet reproduce that on SpeechAnalyzer. Track it and revisit at the next SDK. Second, because there's no server fallback, older iPhones with insufficient storage for the language model will fail the asset download, and you need to surface that gracefully.

The modules: SpeechTranscriber, DictationTranscriber, SpeechDetector

Instead of one monolithic recognizer, SpeechAnalyzer is a coordinator you configure with modules. Each module conforms to SpeechModule and is added at construction time or attached later while a session is running. The three ship-in-the-box modules are:

  • SpeechTranscriber: the flagship module. Runs Apple's new proprietary transcription model. Best for long-form audio, dictation, captions, and podcasts. Around 30 locales at launch.
  • DictationTranscriber: a compatibility bridge. Uses the same on-device engine and language coverage as iOS 10-era SFSpeechRecognizer. Use it when you need a locale that SpeechTranscriber doesn't cover yet, or when you specifically want the older acoustic model's behavior. Unlike the old API, it does not require the user to enable keyboard dictation in Settings.
  • SpeechDetector: a lightweight voice activity detector. It emits events when speech begins and ends but doesn't produce text on its own; it always pairs with a transcriber module. Use it to gate expensive transcription on silence, or to auto-segment long recordings.

Composing modules

import Speech

let transcriber = SpeechTranscriber(
    locale: Locale(identifier: "en-US"),
    transcriptionOptions: [],
    reportingOptions: [.volatileResults, .alternativeTranscriptions],
    attributeOptions: [.audioTimeRange]
)

let detector = SpeechDetector(sensitivityLevel: .medium)

let analyzer = SpeechAnalyzer(modules: [transcriber, detector])

You'll typically hold the transcriber reference on your view model so you can iterate transcriber.results from a Task. Each module owns its own result stream; the analyzer just synchronizes audio delivery. Because SpeechAnalyzer is an actor, adding or removing a module during a live session is a legal, non-blocking operation. Pair that with the mental model from the Swift Actors guide if the isolation rules trip you up.

Permissions and downloading locale assets

Before any transcription happens, two boxes need ticking: user permission for the microphone, and the on-device language model for your target locale. The permission side is unchanged from earlier SDKs. You still add NSMicrophoneUsageDescription to your Info.plist (or the equivalent build setting for Xcode 26 projects), and if you want the transcriber's word-level timing metadata you also add NSSpeechRecognitionUsageDescription. Call AVAudioApplication.requestRecordPermission() before starting the audio engine.

Locale assets are managed through the new AssetInventory API. Ask which locales the current device supports, check whether the model for your target locale is installed, and trigger a download if not. The download runs in the background and reports progress you can surface in UI.

import Speech

func ensureModel(for locale: Locale) async throws {
    let supported = await SpeechTranscriber.supportedLocales
    guard supported.contains(where: { $0.identifier(.bcp47) == locale.identifier(.bcp47) }) else {
        throw TranscriptionError.unsupportedLocale
    }

    let installed = await SpeechTranscriber.installedLocales
    guard !installed.contains(where: { $0.identifier(.bcp47) == locale.identifier(.bcp47) }) else {
        return
    }

    if let request = try await AssetInventory.assetInstallationRequest(
        supporting: [SpeechTranscriber(locale: locale, preset: .transcription)]
    ) {
        try await request.downloadAndInstall()
    }
}

Transcribing an audio file end-to-end

The file case is the shortest path to a working demo. You point the analyzer at an AVAudioFile, iterate its results stream, and finalize when done. The following example transcribes a recording bundled with the app, prints each finalized segment to the console, and returns the full transcript when the file is exhausted.

import AVFoundation
import Speech

func transcribeFile(at url: URL) async throws -> String {
    let file = try AVAudioFile(forReading: url)

    let transcriber = SpeechTranscriber(
        locale: Locale.current,
        preset: .transcription
    )
    let analyzer = SpeechAnalyzer(modules: [transcriber])

    // Consume results concurrently with feeding audio.
    let collectionTask = Task { () -> String in
        var full = ""
        for try await result in transcriber.results {
            if !result.isVolatile {
                full += String(result.text.characters)
            }
        }
        return full
    }

    try await analyzer.analyzeSequence(from: file)
    try await analyzer.finalizeAndFinishThroughEndOfInput()

    return try await collectionTask.value
}

Two details are load-bearing. First, analyzeSequence(from:) handles the audio-format conversion internally when you pass an AVAudioFile, so you don't need to pre-resample. Second, finalizeAndFinishThroughEndOfInput() tells the transcriber that no more audio is coming, which lets it emit any pending volatile results as finals and close the async sequence cleanly. Skip that call and your for try await loop will hang forever waiting for the next chunk. I hit this exact bug shipping a batch-transcription tool last month; the missing finalize was the entire problem.

Live microphone transcription with AVAudioEngine

Live capture is where most real-world integrations live: captions, voice notes, hands-free UI, dictation. The pattern is AVAudioEngine, then tap the input node, convert to the analyzer's preferred format, then push into an AsyncStream the analyzer reads. Ask the transcriber what format it wants via SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith:). On modern devices this is typically 16 kHz mono Float32, while the microphone hands you 48 kHz mono at the hardware level.

import AVFoundation
import Speech

@MainActor
final class LiveTranscriber: ObservableObject {
    @Published var text: String = ""
    @Published var volatileTail: String = ""

    private let engine = AVAudioEngine()
    private var analyzer: SpeechAnalyzer?
    private var transcriber: SpeechTranscriber?
    private var inputBuilder: AsyncStream<AnalyzerInput>.Continuation?
    private var converter: AVAudioConverter?

    func start(locale: Locale = .current) async throws {
        try await ensureModel(for: locale)

        let transcriber = SpeechTranscriber(
            locale: locale,
            transcriptionOptions: [],
            reportingOptions: [.volatileResults],
            attributeOptions: []
        )
        let analyzer = SpeechAnalyzer(modules: [transcriber])
        self.transcriber = transcriber
        self.analyzer = analyzer

        // Configure input stream.
        let (stream, continuation) = AsyncStream<AnalyzerInput>.makeStream()
        self.inputBuilder = continuation
        try await analyzer.start(inputSequence: stream)

        // Consume results.
        Task { [weak self] in
            guard let transcriber = self?.transcriber else { return }
            for try await result in transcriber.results {
                let piece = String(result.text.characters)
                await MainActor.run {
                    if result.isVolatile {
                        self?.volatileTail = piece
                    } else {
                        self?.text += piece
                        self?.volatileTail = ""
                    }
                }
            }
        }

        // Wire microphone.
        let input = engine.inputNode
        let hwFormat = input.outputFormat(forBus: 0)
        let targetFormat = await SpeechAnalyzer.bestAvailableAudioFormat(
            compatibleWith: [transcriber]
        ) ?? hwFormat
        converter = AVAudioConverter(from: hwFormat, to: targetFormat)

        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.record, mode: .measurement, options: [])
        try session.setActive(true)

        input.installTap(onBus: 0, bufferSize: 4096, format: hwFormat) { [weak self] buffer, when in
            guard let self, let converter = self.converter else { return }
            let ratio = targetFormat.sampleRate / hwFormat.sampleRate
            let capacity = AVAudioFrameCount(Double(buffer.frameLength) * ratio) + 32
            guard let converted = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: capacity) else { return }
            var error: NSError?
            converter.convert(to: converted, error: &error) { _, status in
                status.pointee = .haveData
                return buffer
            }
            if error == nil {
                self.inputBuilder?.yield(AnalyzerInput(buffer: converted))
            }
        }

        engine.prepare()
        try engine.start()
    }

    func stop() async throws {
        engine.inputNode.removeTap(onBus: 0)
        engine.stop()
        inputBuilder?.finish()
        try await analyzer?.finalizeAndFinishThroughEndOfInput()
        try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
    }
}

Volatile vs. final results in the SwiftUI layer

Each element in transcriber.results exposes an isVolatile flag. Volatile results are the transcriber's current best guess for the last few hundred milliseconds; they will be revised as more audio arrives. Final results are locked in and will never change. Rendering the two in one SwiftUI Text without distinguishing them causes the classic caption jitter (words flip mid-sentence) that ships in a lot of hobby demos.

The pattern I use is to keep an appended-to String of final text plus a small volatile tail that gets replaced on every update. Render the tail in a slightly lighter foreground so users get a subtle visual cue that the words are still being decided.

struct CaptionView: View {
    @ObservedObject var transcriber: LiveTranscriber

    var body: some View {
        HStack(alignment: .lastTextBaseline, spacing: 4) {
            Text(transcriber.text)
                .foregroundStyle(.primary)
            Text(transcriber.volatileTail)
                .foregroundStyle(.secondary)
        }
        .font(.title3.monospaced())
        .animation(.smooth(duration: 0.15), value: transcriber.volatileTail)
    }
}

If you plan to feed the transcript into Apple's on-device summarization or generative features, the exact same buffer works. Pass the finalized text straight into the model. The Apple Foundation Models guide covers the follow-on step of turning a transcript into a summary or action items on device.

Platform differences: iPad, Mac Catalyst, visionOS, tvOS

The API surface is identical across platforms. The sharp edges are all in audio input and system integration. So, here's what I've hit shipping the same transcription code across the SDK.

iPadOS

Identical to iPhone. Stage Manager and split view don't affect the analyzer directly, but be careful with AVAudioSession interruptions when the app is backgrounded next to another audio-producing app. You still get the standard interruption notification and the tap stops delivering buffers.

Mac Catalyst and native macOS

On macOS 26 the microphone permission prompt is scoped to the app, not the device, and system settings live under Privacy & Security → Microphone. The analyzer runs in a system XPC service that is shared across apps, so a second app requesting the same locale reuses the already-installed model. No duplicate 200 MB download. Mac Catalyst apps see the iOS-style permission alert but hit the macOS system service under the hood.

visionOS

visionOS 26 exposes the same API but treats the microphone as a shared spatial audio resource. Ambient mic input is noisier than a phone held near the mouth. Bump the SpeechDetector sensitivity to .high and expect a slightly higher final-latency budget. Dictation for text fields uses the system dictation pipeline (DictationTranscriber under the hood) rather than your custom analyzer session.

tvOS

tvOS 26 supports SpeechAnalyzer for accessory microphone input (AirPods, HomePod-style far-field mics). There is no built-in mic on Apple TV hardware, so AVAudioSession.availableInputs is worth checking before you offer transcription UI at all.

watchOS

Not supported in the current SDK. If your Watch app needs speech-to-text today, stay on SFSpeechRecognizer. It continues to ship and work on watchOS 26.

Common pitfalls and migrating from SFSpeechRecognizer

A short field-report from migrating a shipping app:

  • Don't skip the finalize step. Without finalizeAndFinishThroughEndOfInput(), the volatile tail is never promoted to final, the async sequence never closes, and your consumer Task lives forever.
  • Handle asset installation errors. A user on a 64 GB device with almost no free space will fail the model download. Detect this at ensureModel and offer a graceful fallback (record raw audio, sync later, or fall back to DictationTranscriber if its locale is present).
  • Don't reuse an analyzer across sessions. Each call to finalizeAndFinishThroughEndOfInput() terminates the analyzer's input stream. Build a fresh SpeechAnalyzer per recording. Construction is cheap because the model lives in the shared XPC process.
  • Test with airplane mode on. The whole point of the framework is that it's on-device; verifying that end-to-end without a network keeps you honest about the asset-download flow.
  • Match reporting options to your UX. Requesting .alternativeTranscriptions and .audioTimeRange costs measurable CPU and memory. Only enable what your UI actually renders.

For migration itself, the mechanical part is straightforward. Replace SFSpeechAudioBufferRecognitionRequest with your AsyncStream<AnalyzerInput>, replace the delegate/callback with a for try await loop, and route your existing AVAudioEngine tap through an AVAudioConverter. Keep SFSpeechRecognizer around behind an #available check for watchOS and for any custom-vocabulary path you can't yet replace. Apple's Bringing advanced speech-to-text capabilities to your app documentation covers the reference API in depth, and the WWDC25 session 277 walks through the architecture decisions if you prefer video.

If your recording pipeline is broader than just transcription (capturing to disk, mixing, syncing with the network), the same async-first patterns from Modern Swift Networking with async/await compose well with the analyzer's AsyncSequence-based output.

Frequently Asked Questions

Does SpeechAnalyzer work offline?

Yes. SpeechAnalyzer is on-device only and has no server fallback path. The first-time download for a locale model requires a network connection, but once installed, transcription runs entirely offline in a system XPC process outside your app's memory.

Is SpeechAnalyzer available on watchOS 26?

No. As of the iOS/watchOS 26 SDK, SpeechAnalyzer, SpeechTranscriber, DictationTranscriber, and SpeechDetector are unavailable on watchOS. Continue using SFSpeechRecognizer for Watch apps that need speech-to-text.

What languages does SpeechTranscriber support?

Roughly 30 locales at iOS 26 launch, with more being added over time. Query SpeechTranscriber.supportedLocales at runtime. If your target locale isn't in the list, fall back to DictationTranscriber, which uses the broader iOS-10-era dictation language set.

What is the difference between SpeechTranscriber and DictationTranscriber?

SpeechTranscriber uses Apple's new proprietary long-form model, so you get higher accuracy, better handling of distant mics and long audio, and about 30 locales. DictationTranscriber uses the same engine as iOS 10-era on-device SFSpeechRecognizer, giving you wider legacy language coverage but lower accuracy and no long-form advantage.

Can SpeechAnalyzer transcribe audio files as well as live microphone input?

Yes. Pass an AVAudioFile to analyzer.analyzeSequence(from:) for file transcription, or push AnalyzerInput buffers into an AsyncStream from an AVAudioEngine tap for live mic input. Both paths use the same modules and result stream.

Does SpeechAnalyzer support custom vocabulary hints?

Not yet. If your app relies on SFSpeechRecognitionRequest.contextualStrings to boost recognition of domain-specific terms (medical, legal, gaming, brand names), stay on SFSpeechRecognizer for that flow. Apple has not announced timing for adding this capability to the new API.

Hiroshi Sato
About the Author Hiroshi Sato

Apple Platforms specialist building for iOS, macOS, visionOS, and the occasional watchOS app nobody asked for.