Pattern · Prompt Input

New

Dictation Composer SwiftUI Block

A message composer with voice input: a live level meter, elapsed time, Cancel and Done, and the transcript filling the field.

A composer pinned above the keyboard whose mic turns the bar into a recording state, with a level meter that moves with the voice, the elapsed time, Cancel and Done, while the transcript fills the field. It runs on the Speech framework and AVAudioEngine, asks for access and offers Settings when it is off, and takes your own transcription for previews or another engine.

  • SwiftUI
  • iOS 17.0+
  • composer-dictation
Get SwiftPieces Pro

$199 once, every piece included. See how installing works · Sign in

01

What's included

SwiftUI source you own, built on Apple frameworks only.

  • The mic turns the bar into Cancel, a live level meter, the time and Done
  • The transcript fills the field as you speak, after anything typed
  • Speech framework and AVAudioEngine, with access asked and Open Settings when off
  • Your own transcription through one value, or a scripted one for previews
  • Send turns into Stop while the reply runs

02

Implementation notes

How it behaves, what it handles, and the decisions behind it.

The composer is one card pinned above the keyboard: a field that grows from one line to six, with the mic and Send on the row under it. Tapping the mic asks for speech recognition and the microphone the first time, then turns the row into a recording state: Cancel on the left, a red dot with the elapsed time and a level meter of thin bars that rise and fall with your voice, and Done in the accent on the right, while the card's hairline turns to ink and the field fills with the transcript as it is heard. Words land after anything already typed. Done keeps them, Cancel puts the field back as it was, and dictation finishes on its own at one minute. When speech recognition or the microphone is off, a line under the card says so with Open Settings; when dictation is restricted or unavailable it says that instead. Send stays off until there is text; while your onSend runs, Stop takes its place and cancels the task, and a failed send hands the text back with the error under the card.

Notes

  • DictationComposer(text:placeholder:transcription:maxDuration:onSend:). Only onSend is required. It is a section: no navigation stack, background, padding or scroll view of its own. Pin it with .safeAreaInset(edge: .bottom) and .padding(.horizontal, 20) on a theme.color.bg background.
  • onSend receives the text, trimmed. Stop cancels the task and frees the composer at once; any other thrown error shows its errorDescription, or "Couldn't send. Try again.", and puts the text back.
  • DictationComposer.Transcription is the engine behind the mic: access (whether it may run now), requestAccess (asks, returning the result) and start (an AsyncThrowingStream of Updates, each with the whole transcript so far, a level from 0 to 1, or both). The block cancels the stream on Done and Cancel; the stream may also end by itself, which counts as Done.
  • .speech(locale:), the default, runs SFSpeechRecognizer with partial results and punctuation on microphone audio from an AVAudioEngine tap, measuring each buffer's loudness for the meter. It asks for speech recognition, then the microphone, and stops the engine and releases the audio session when the stream ends. .scripted(_:wordsPerSecond:) plays a fixed transcript with a moving level, for previews, tests and the simulator; Transcription.sample is one.
  • DictationComposer.Access is .allowed, .notDetermined, .denied, .restricted or .unavailable. Denied shows "Turn on Microphone and Speech Recognition in Settings to dictate." with Open Settings, which opens your app's page in Settings.
  • DictationComposer.TranscriptionError is .unavailable ("Dictation isn't available right now.") or .interrupted ("Dictation stopped. Try again."); the default engine reports recognizer failures as .interrupted. A failure keeps what was heard so far in the field.
  • maxDuration is one minute by default, the Speech framework's own limit for one request.
  • text is a Binding<String> for a draft your app keeps.
  • Haptics: a start tap when recording begins, success on Done, a light impact on Send, an error on a refusal or failure.
  • VoiceOver reads the field with the transcript as its value ("Listening" until words come), the recording row as "Recording" with the time, and Cancel dictation and Done with hints. Refusals, "Dictation added", "Stopped" and failures are announced. Every control has a 44 point target.
  • Reduce Motion keeps the meter still between readings and turns state changes into short fades.
  • DictationComposerExample at the end of DictationComposer.swift is the Kitchen thread from the preview. On a device the mic uses the Speech framework; in the simulator and in Xcode previews it plays a scripted transcript. Sending holds the reply for two seconds so Stop can be tried. Pass wrapsInNavigationStack: false to push it in your own stack.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • The card is color.surface with radius.lg and a 1 point separator hairline that turns to textPrimary while the field is focused or recording. The field is type.body in textPrimary, its prompt textTertiary.
  • The mic and Cancel are 36 point fill circles with textPrimary symbols in 44 point targets. Send and Done are the accent: accent circles with accentOn symbols, Send on fill with textTertiary while off. Only one of them shows at a time.
  • The meter's bars are textPrimary; the recording dot is danger, a real status. The time is type.subhead with monospaced digits in textSecondary.
  • The access line is type.footnote in textSecondary with Open Settings in type.subhead semibold textPrimary; errors are type.footnote in danger.
  • State changes use motion.smooth and motion.snappy, and become short fades under Reduce Motion.

Requirements

  • NSMicrophoneUsageDescription and NSSpeechRecognitionUsageDescription in Info.plist, for the default .speech() transcription.