RORK LABEN
FUNDING — Rorkの$15MシードはLeft Lane Capital主導で、Peak XV・True Ventures・Goodwater・a16z Speedrunが参加していますGROWTH — Rorkは月間74.3万訪問・成長率85%と、利用の伸びが続いていますMAX — Rork MaxはネイティブSwiftアプリを生成し、iPhone・iPad・Watch・TV・Vision Pro・iMessageに対応しますMAX — HealthKit・Core ML・Dynamic Islandなど、React Nativeでは届きにくい領域に踏み込めますMARKET — AppleもXcode 27でエージェント型コーディングを推進し、AIがネイティブ開発を担う流れが加速していますMARKET — Gartnerは2026年末までに新規アプリの75%が低コード/ノーコード製になると予測していますFUNDING — Rorkの$15MシードはLeft Lane Capital主導で、Peak XV・True Ventures・Goodwater・a16z Speedrunが参加していますGROWTH — Rorkは月間74.3万訪問・成長率85%と、利用の伸びが続いていますMAX — Rork MaxはネイティブSwiftアプリを生成し、iPhone・iPad・Watch・TV・Vision Pro・iMessageに対応しますMAX — HealthKit・Core ML・Dynamic Islandなど、React Nativeでは届きにくい領域に踏み込めますMARKET — AppleもXcode 27でエージェント型コーディングを推進し、AIがネイティブ開発を担う流れが加速していますMARKET — Gartnerは2026年末までに新規アプリの75%が低コード/ノーコード製になると予測しています
記事一覧/アプリ開発
アプリ開発/2026-07-03中級

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

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

Rork Max209AVSpeechSynthesizer読み上げアクセシビリティ4SwiftUI60

プレミアム記事

読書アプリに「読み上げ」を付けてほしい、という要望をもらったとき、最初は数行で終わると思っていました。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-07-02
Rork Max × MapKit で場所検索マップを実装する — MKLocalSearch・ピン集約・位置情報許可の設計
Rork Max のネイティブ Swift で場所検索マップ画面を作る実装手順です。MKLocalSearch の周辺検索、ピンが増えたときの SwiftUI Map と MKMapView の使い分け、位置情報許可と審査対策まで、試作で実際に詰まった点を交えて整理します。
アプリ開発2026-07-01
Rork Max のネイティブ Swift で SharePlay を組む — 二人の画面を同じ状態で動かす設計
GroupActivities を使い、FaceTime 越しに二人の画面を同じ状態で動かす SharePlay を Rork Max のネイティブ Swift で組む実装メモです。GroupActivity の宣言、GroupSession への参加、GroupSessionMessenger での状態同期、遅延と競合の扱い、そして React Native 側から橋渡しするときの境界設計まで、実際に詰まった点を添えてまとめます。
アプリ開発2026-07-01
Rork Max のネイティブ Swift で ShazamKit を使い、流れている曲を認識するアプリを作る
SHManagedSession を使って、周囲で流れている曲を認識するアプリを Rork Max のネイティブ Swift で組む実装メモです。旧来の AVAudioEngine 手回しとの違い、idle・prerecording・matching の状態設計、認識精度を上げる prerecording の使いどころ、Expo からの橋渡しの境界設計まで、実際にハマった点を添えてまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →