The iPhone app and the keyboard it ships. Same engine as the Mac app
(Sources/BlurtEngine), a different shell, because iPhone leaves no other shape:
- The app listens and transcribes. iOS lets no keyboard use the microphone. The
app opens the microphone while it is in front (the keyboard sends it there with
blurt://start), keeps it open for a listening window (ListeningWindow, default 15 minutes, extended by every dictation), and runs the engine'sDictationSessionover that always-on capture (WindowedAudioSource, which converts the phone's native audio to the 16 kHz mono PCM the dictation API wants). - The keyboard is a remote control. It sends press / release / cancel with the
text around the cursor (the same context the Mac reads through Accessibility),
shows the phase the app publishes, inserts the words that come back through
textDocumentProxy, and reads the phone's word list (UILexicon: contact names and text replacements) so names come back spelled right with no setup. - They talk through an App Group (
BlurtiOSCore/SharedState.swift): payloads in the shared defaults, Darwin notifications to wake each other. Full Access is what lets the keyboard reach the group — without it the keyboard still types, and says what it needs. - Three layouts, the user's choice: a slim mic bar, a mic panel (the Wispr / Aqua shape), or a full keyboard with the mic as the main key. One keyboard target, one setting, one set of plumbing.
cd App/BlurtiOS && xcodegen generate # the project is generated, not committed
open BlurtiOS.xcodeproj # scheme BlurtiOS builds and embeds the keyboardCI builds it for the simulator on every PR (check.yml's ios-build job). On a
Mac with only the Command Line Tools — no iOS SDK — scripts/ios-typecheck.sh
typechecks both targets against the Mac Catalyst frameworks with the same
flags, which catches the Swift 6 isolation errors before CI does. Running
on a phone needs Xcode with a team that can provision the App Group. The ids in
project.yml (com.assemblyai.blurt.iphone, com.assemblyai.blurt.iphone.keyboard,
group.com.assemblyai.blurt.iphone) are AssemblyAI's, so they register under the AssemblyAI team;
anyone building under another team changes them locally (see Testing on a phone).
DESIGN.md — the tokens, the orb, the meter, the three layouts, the
app's screens, all traced to the Mac app's sources, plus the gallery that renders every layout
and state for a screenshot and the launch flags that reach every app state.
Any Xcode from 26.6 (CI's macos-26 runner has it) works; a phone on iOS 27 needs Xcode 27, which
needs macOS 26.6. The Mac App Store is one way. The other is Apple's developer site through
xcodes: Homebrew cannot build it without Xcode
already present (chicken and egg), so take the prebuilt binary —
gh release download --repo XcodesOrg/xcodes --pattern xcodes.zip, unzip, put xcodes on
PATH — then xcodes install 27.0 --select. It signs in with an Apple ID that has accepted
the Apple Developer Agreement at developer.apple.com/account; a company-managed Apple ID
(Apple Business Manager) cannot accept it and gets a 403, so use a personal one for the
download. The archive lands in ~/Library/Application Support/com.robotsandpencils.xcodes/;
an 83 KB "xip" there is a saved error page, delete it and retry.
The iPhone code splits the way the Mac's does. BlurtiOSCore/ is the logic — the App Group
contract, the keyboard's model and rules, the voice state, term packs, the relay injector — and
plays the part BlurtEngine plays for the Mac shell: no SwiftUI, no views, no audio (the keyboard
links it, and the keyboard never hears anything; check-invariants.sh enforces the last). The
app (BlurtiOS/Sources) and the keyboard (BlurtKeyboard/Sources) are the UI on top of it, drawn
with BlurtDesign/, the component library: the design tokens, type roles and brand colours,
and the pieces every screen is built from (card, page, form, eyebrow, button, wordmark, grain).
It is a static framework, so the app and the keyboard each link their own copy beside the
engine's, and its API is package (one SWIFT_PACKAGE_NAME across the project), not public:
nothing outside this project can see it, and periphery still reports what nothing uses. A
preview hands the model a ThemeFace, not a palette — the colours are the UI's
(KeyboardModel+Palette.swift).
scripts/ios-test.sh runs BlurtiOSTests on a simulator (CI's ios-build job runs it after
the build): the core's logic — the App Group contract (key terms, stale snapshots, layouts),
the keyboard's rules (sentence start, the double space, the term field, return labels, letter
rows) against a fake text field, the gate against the app's phases, the Darwin signals, term
packs — and what the UI builds on it (palettes, the voice element, the gallery). The engine's
own tests stay swift test. Anything with a rule in DESIGN.md should have a test here.
The suite holds the same gates as the engine's: warnings are errors, a plain run fails when
BlurtiOSCore drops below the engine's own line-coverage floor (MIN_IOS_COVERAGE, 88%, in the
script — raise it as coverage grows, never lower it to land a change), and CI's ios-sanitizers
job runs it again under ThreadSanitizer and AddressSanitizer (BLURT_IOS_SANITIZER=thread or
address locally). scripts/ios-check.sh runs the whole iPhone bar in one go — the tests and
the coverage gate, then swiftlint analyze and periphery over this project — and is what CI's
ios-build job and a local scripts/check.sh run. The views are not in that figure, as the Mac shell's aren't in the engine's:
they are checked on sight, through the gallery and the probe's screenshot flows.
Signing and the team don't matter here, and the App Group works, so the whole
loop runs (scripts/ios-lib.sh holds the one signing recipe and the simulator pick). AVCaptureSession carries no audio in the simulator, so the app uses
SimulatorAudioSource (an Audio Queue on the Mac's microphone) there and
WindowedAudioSource on a phone.
scripts/ios-sim.sh # generate, build, boot, install, grant mic, launch
scripts/ios-sim.sh --screenshot shot.png # …and capture the screenXcode 27 replaced Simulator.app with Device Hub (Xcode.app/Contents/Applications/DeviceHub.app),
which the script opens; turn off its Always simulate hardware keyboard setting (Device Hub →
Settings…) or no on-screen keyboard ever appears. Then: Blurt → Open Settings, and Apps → Blurt →
Keyboards → Blurt on, Allow Full Access (the simulator's Settings opens at its top level, not on
Blurt's page); turn the session on; in Notes, hold the globe key, pick Blurt, tap the
mic. The keyboard's crash logs, if any, land in ~/Library/Logs/DiagnosticReports/BlurtKeyboard-*,
and xcrun simctl spawn booted log show --last 5m --predicate 'process == "BlurtKeyboard"'
shows its console.
-
Install Xcode 26.6 or newer — CI's runner has 26.6 (
xcodes install 26.6 --select, or the Mac App Store). A phone on iOS 27 needs Xcode 27, which needs macOS 26.6. Then once:sudo xcode-select -s /Applications/Xcode.app sudo xcodebuild -license accept sudo xcodebuild -runFirstLaunch xcodebuild -downloadPlatform iOS # only for the simulator; a phone needs no downloadscripts/bootstrap.shfrom the repo root installs xcodegen and the linters ifbrewhas not. -
Sign in to Xcode with your Apple ID (Settings → Accounts) and note your team ID (the ten characters after the team's name). The App Group needs a paid team — a free Personal Team cannot register one, so use the org's team, not your own.
-
cd App/BlurtiOS && BLURT_TEAM=<your team id> xcodegen generate && open BlurtiOS.xcodeproj. Automatic signing registerscom.assemblyai.blurt.iphone,com.assemblyai.blurt.iphone.keyboardandgroup.com.assemblyai.blurt.iphoneunder that team on the first build; it fails if another team already owns them. -
Plug the phone in (Developer Mode on: Settings → Privacy & Security), pick it as the run destination, run the
BlurtiOSscheme. On the phone, trust the developer app (Settings → General → VPN & Device Management). In the app: Paste your key, Allow sending your voice to AssemblyAI, Allow the microphone, then Open Settings and go to Apps › Blurt › Keyboards (a phone may open straight on Blurt's page): turn on Blurt and Allow Full Access. -
Back in the app, turn the session on, then go to Messages, hold the globe key, pick Blurt, and tap the mic. Tap again to stop, or hold it while you talk. The words land in the field.
-
Try the other two layouts from the app's Keyboard picker; the keyboard reads the choice the next time it comes up.
-
Autocorrect and the suggestion bar on the full keyboard.
-
Sign in with AssemblyAI. The shipping app will sign the user in and get a key behind the scenes; that service does not exist yet. Until it does, every build (TestFlight included) takes a pasted API key (
KeyEntryView), as the Mac app does. -
Returning to the host app. After "Start Blurt" the user swipes back; iOS 26.4 ended automatic switch-back for everyone.
-
The full keyboard has no autocorrect or suggestions yet.
The listening window's life in the background (capture keeps flowing after the swipe back; battery over an hour); keyboard → app → back on iOS 26 and 27; the keyboard's memory with the orb growing into the wave; how long the lexicon takes on a phone with thousands of contacts; and, once the service exists, sign-in end to end.