◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/App Dev
◇ App Dev/2026-07-03Intermediate

Adding Read-Aloud to a Rork Max App: AVSpeechSynthesizer Voice Selection and Live Word Highlighting

An implementation memo on adding read-aloud to a native Swift app generated by Rork Max — covering AVSpeechSynthesizer voice selection, highlighting the word being spoken, audio session design, and the pitfalls that bite specifically with Japanese text.

Rork Max236AVSpeechSynthesizerRead AloudAccessibility4SwiftUI66

✦ Premium Article

When someone asked me to add read-aloud to a reading app, I assumed it would be a few lines. As an indie developer I tend to underestimate these small features. Hand text to AVSpeechSynthesizer and it talks — that part is true. But on a real device the small complaints piled up fast: the voice sounded muffled, you couldn't tell where on screen it was reading, and background music cut out abruptly. Making it speak is trivial; making it something you actually enjoy listening to takes several deliberate design choices.

Here I've written down, in the order I actually touched them, the steps for wiring read-aloud into the native Swift apps Rork Max produces. We start from the smallest working version, then move to choosing a voice, highlighting the word being spoken, tuning the audio session, and finally the things that tripped me up specifically in Japanese.

Make it speak — but keep the synthesizer alive

This is the minimal version. There's exactly one landmine every beginner hits: if you create AVSpeechSynthesizer as a local variable, it gets deallocated before it finishes speaking and you get silence. The rule is to hold it outside the view, inside an object that survives — a stored property.

import AVFoundation
import SwiftUI
 
@MainActor
final class Reader: ObservableObject {
    // ❌ A local variable inside a func is released before it speaks -> silence
    // ✅ Keep it as a stored property
    private let synth = AVSpeechSynthesizer()
 
    func speak(_ text: String) {
        let utterance = AVSpeechUtterance(string: text)
        utterance.rate = AVSpeechUtteranceDefaultSpeechRate  // ~0.5
        utterance.pitchMultiplier = 1.0
        utterance.postUtteranceDelay = 0.2
        synth.speak(utterance)  // enqueued and read in order
    }
 
    func stop() {
        synth.stopSpeaking(at: .immediate)
    }
}

speak(_:) enqueues rather than plays immediately. Call it repeatedly and utterances are read in order, so to "interrupt the current one and read the next," call stopSpeaking(at:) first. .immediate stops right away; .word finishes the current word before stopping. When the user taps the next sentence, .immediate felt right; for a pause button, pauseSpeaking(at: .word) was the natural choice.

Choosing a voice — installed voices differ in quality

This is what moves the perceived quality the most. If you specify only a language with AVSpeechSynthesisVoice(language:), you get that language's default voice, which isn't always the pleasant one. iOS mixes compact lightweight voices with higher-quality downloaded ones (.enhanced or .premium), and preferring the latter when present raises satisfaction noticeably.

extension AVSpeechSynthesisVoice {
    /// Returns the highest-quality voice for a language (premium > enhanced > default)
    static func bestVoice(for language: String) -> AVSpeechSynthesisVoice? {
        let candidates = speechVoices().filter {
            $0.language.hasPrefix(language)  // catches both "ja-JP" and "ja"
        }
        // quality is .default(1) < .enhanced(2) < .premium(3)
        return candidates.max(by: { $0.quality.rawValue < $1.quality.rawValue })
    }
}
 
// Usage
let voice = AVSpeechSynthesisVoice.bestVoice(for: "ja")
        ?? AVSpeechSynthesisVoice(language: "ja-JP")
utterance.voice = voice

The catch: high-quality voices may not be installed. Unless the user added them under Settings → Accessibility → Spoken Content → Voices, .premium never appears among the candidates. I placed a low-key hint inside the app ("for a more natural voice, add one from Settings") and made "doesn't break with the default voice" the floor. Design that assumes premium quality and the experience collapses on devices that never added it.

The relationship between quality and availability looks like this:

qualityCharacterHow to get itDesign stance
defaultLightweight, a bit roboticAlways present in the OSGuarantee it as the fallback floor
enhancedNatural, tens of MBManual download in SettingsPrefer if present; don't break without it
premiumMost natural (iOS 16+)Manual download in SettingsTop priority if present; never assume it
✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦The delegate wiring that highlights the word currently being spoken, and why the range drifts on text with emoji
✦Detecting the quality tier of voices installed on the device so you prefer the most natural one available
✦The audio session setup that reads aloud without killing the user's background music, plus lock-screen behavior
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◇ App Dev2026-08-06
Deciding overlay text legibility at ingest time instead of on device — four metrics measured side by side
Moving the question of whether text stays readable over a wallpaper out of the device and into the content pipeline. Four candidate metrics measured across 240 images, including what downscaled judging actually computes.
◇ App Dev2026-04-26
Native iOS Apps with Rork: SwiftUI to TestFlight
New iPhone projects in Rork are generated in Swift and SwiftUI, and Rork Pro carries them all the way to the App Store. How to tell an old Expo project from a new one, costs in credits, the skeleton prompt, device testing, when to drop into Xcode, and TestFlight submission.
⬡ Dev Tools2026-07-18
The Generated Screen That Quietly Jams at AX5 and in German — Putting Layout Resilience Checks on Rork Max SwiftUI
A record of running every Rork Max generated SwiftUI screen through pseudolocalization and the largest text size to find exactly where it jams. Covers when to reach for ViewThatFits, ScaledMetric and layoutPriority, plus the snapshot checks that catch regressions every time you regenerate.