◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/アプリ開発
◇ アプリ開発/2026-07-03中級

Rork Max アプリに読み上げ機能を組み込む — AVSpeechSynthesizer の音声選択と発話中ハイライトの設計

Rork Max のネイティブ Swift アプリに読み上げ機能を組み込む実装メモです。AVSpeechSynthesizer の音声選択、発話中の単語ハイライト、オーディオセッションの設計、そして日本語で詰まりやすい落とし穴までまとめました。

Rork Max236AVSpeechSynthesizer読み上げアクセシビリティ5SwiftUI66

✦ プレミアム記事

読書アプリに「読み上げ」を付けてほしい、という要望をもらったとき、最初は数行で終わると思っていました。AVSpeechSynthesizer にテキストを渡せば喋る、それは事実です。ところが実機で試すと、声が妙にこもっていたり、読んでいる箇所が画面のどこか分からなかったり、BGM が突然止まったりと、細かい不満が次々に出てきました。喋らせること自体は簡単でも、「気持ちよく聞ける読み上げ」にするには設計がいくつも要ります。

ここからは、Rork Max が出力するネイティブ Swift アプリに読み上げを組み込むとき、個人開発で実際に手を入れた順番のまままとめます。テキストを喋らせる最小実装から始めて、音声の選び方、発話中の単語ハイライト、オーディオセッションの調整、最後に日本語で特につまずいた点まで進みます。

まず喋らせる、ただし合成器は持ち続ける

最小の実装はこれだけです。ただし一つだけ、初心者が必ず踏む地雷があります。AVSpeechSynthesizer をローカル変数で作ると、喋り終わる前に解放されて無音になります。View の外、あるいは @State で保持されるオブジェクトの中に置くのが鉄則です。

import AVFoundation
import SwiftUI
 
@MainActor
final class Reader: ObservableObject {
    // ❌ func 内のローカル変数にすると発話前に解放されて無音になる
    // ✅ プロパティとして保持する
    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)  // キューに積まれ順番に読まれる
    }
 
    func stop() {
        synth.stopSpeaking(at: .immediate)
    }
}

speak(_:) は即再生ではなくキューへの追加です。連続で呼ぶと積まれた順に読まれるので、「今の発話を中断して次を読む」ときは先に stopSpeaking(at:) を呼びます。.immediate はその場で止め、.word は今読んでいる単語の切れ目まで読んでから止まります。ユーザーが次の文をタップしたときは .immediate、一時停止ボタンなら pauseSpeaking(at: .word) が自然でした。

声を選ぶ — 端末に入っている音声には品質差がある

ここが体感を最も左右します。AVSpeechSynthesisVoice(language:) で言語だけ指定すると、その言語の「既定の声」が使われますが、これが必ずしも聞きやすい声とは限りません。iOS には圧縮された軽量音声と、追加ダウンロードされた高品質音声(.enhanced や .premium)が混在していて、後者があるなら優先したほうが満足度が上がります。

extension AVSpeechSynthesisVoice {
    /// 指定言語で最も品質の高い音声を返す(premium > enhanced > default)
    static func bestVoice(for language: String) -> AVSpeechSynthesisVoice? {
        let candidates = speechVoices().filter {
            $0.language.hasPrefix(language)  // "ja-JP" と "ja" の両方を拾う
        }
        // quality は .default(1) < .enhanced(2) < .premium(3)
        return candidates.max(by: { $0.quality.rawValue < $1.quality.rawValue })
    }
}
 
// 使用例
let voice = AVSpeechSynthesisVoice.bestVoice(for: "ja")
        ?? AVSpeechSynthesisVoice(language: "ja-JP")
utterance.voice = voice

注意したいのは、高品質音声は端末に入っていない場合があることです。ユーザーが「設定 → アクセシビリティ → 読み上げコンテンツ → 声」で追加していなければ、.premium は候補に現れません。私はアプリ内に「より自然な声をダウンロードするには設定から追加してください」という控えめな導線を置き、既定音声でも破綻しないことを最低ラインにしました。品質を前提にした設計にすると、追加していない端末で一気に体験が落ちます。

音声の品質と入手性の関係を整理すると次のようになります。

quality特徴入手方法設計上の扱い
default軽量・やや機械的OS 標準で常に存在フォールバックの下限として保証する
enhanced自然・容量数十MB設定から手動ダウンロードあれば優先。無くても破綻させない
premium最も自然(iOS 16+)設定から手動ダウンロードあれば最優先。存在は前提にしない
✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦発話に合わせて読んでいる単語をハイライトする delegate 配線と、日本語テキストで range がずれる原因への対処
✦端末に入っている音声の品質差を見分けて、聞き取りやすい声を優先的に選ぶ実装
✦他アプリの音楽を止めずに読み上げるためのオーディオセッション設定と、ロック画面での挙動
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

◇ アプリ開発2026-08-06
壁紙の上の文字が読めるかを、端末ではなく取り込み時に決める — 判定指標を4つ測り比べた記録
壁紙の上に置くステータスバーや見出しが読めるかを、端末での画像解析ではなく取り込み時のメタデータで決める設計です。240枚のコーパスで判定指標を4つ実測し、縮小画像での判定が何と一致するのかまで記録しました。
◇ アプリ開発2026-04-26
Rork でネイティブ iOS アプリを作る — SwiftUI 生成から TestFlight 配信までの実践ワークフロー
Rork の新規 iPhone プロジェクトは Swift/SwiftUI で生成され、Rork Pro で App Store まで届きます。古い Expo プロジェクトとの見分け方、クレジットで見る費用、骨格プロンプト、実機確認、Xcode に降りる基準、TestFlight 提出までを手順で追います。
⬡ 開発ツール2026-07-18
文字サイズ最大とドイツ語で、生成された画面は静かに詰まる — Rork Max の SwiftUI にレイアウト耐性の検証を通した記録
Rork Max が生成した SwiftUI 画面を疑似ローカライズと最大文字サイズに通し、どこから詰まるかを機械的に洗い出した記録です。ViewThatFits・ScaledMetric・layoutPriority の使いどころと、再生成のたびに回帰を捕まえるスナップショット検証を、実際の数値とともにまとめました。