GEMINI LABEN
ROBOTICS — 8月31日に停止した ER 1.6 preview には後継があります。Gemini Robotics ER 2 が公開プレビュー中で、通常版とストリーミング版の2種類が提供されていますVIDEO — ER 2 の成功・失敗判定は静止画ではなく生の映像フィード上で動きます。こぼれ・滑り・位置ずれのような、実行の途中で起きる失敗を捉えられる設計ですDEADLINE — 次の期限は9月30日、gemini-omni-flash-preview の廃止です。移行先は8月27日に GA になった gemini-omni-1.1-flash で、残り4週間を切りましたAPIKEY — 残りの標準 API キーは、制限付きのものも含めて9月中に全面停止します。移行先は Google Cloud サービスアカウントに紐付く auth キー形式ですPRICE — Gemini 3.7 Flash の導入価格 $0.75/$3.75 per 1M は12月31日までです。2027年1月1日から $1.50/$7.50 になるため、年を跨ぐ見積もりは2本立てが要りますAUDIO — Gemini 3.5 Transcribe は85言語以上の言語検出、話者ダイアライゼーション、単語単位タイムスタンプ、最大1,000語のカスタム語彙バイアスに対応していますROBOTICS — 8月31日に停止した ER 1.6 preview には後継があります。Gemini Robotics ER 2 が公開プレビュー中で、通常版とストリーミング版の2種類が提供されていますVIDEO — ER 2 の成功・失敗判定は静止画ではなく生の映像フィード上で動きます。こぼれ・滑り・位置ずれのような、実行の途中で起きる失敗を捉えられる設計ですDEADLINE — 次の期限は9月30日、gemini-omni-flash-preview の廃止です。移行先は8月27日に GA になった gemini-omni-1.1-flash で、残り4週間を切りましたAPIKEY — 残りの標準 API キーは、制限付きのものも含めて9月中に全面停止します。移行先は Google Cloud サービスアカウントに紐付く auth キー形式ですPRICE — Gemini 3.7 Flash の導入価格 $0.75/$3.75 per 1M は12月31日までです。2027年1月1日から $1.50/$7.50 になるため、年を跨ぐ見積もりは2本立てが要りますAUDIO — Gemini 3.5 Transcribe は85言語以上の言語検出、話者ダイアライゼーション、単語単位タイムスタンプ、最大1,000語のカスタム語彙バイアスに対応しています
記事一覧/API / SDK
API / SDK/2026-05-26中級

iOS の URLSession で Gemini API のストリーミングがバックグラウンド復帰時に途切れる原因と対処

iOS アプリで Gemini API のストリーミング応答を受け取っている最中にアプリを一旦バックグラウンドに送って戻ると、接続が切れて続きが返ってこない――そんな現象に遭遇したときの原因切り分けと、URLSession 周りの最小修正をまとめます。

Gemini API228iOS13Swift3URLSessionストリーミング13トラブルシューティング31

iOS アプリで Gemini API のストリーミング応答を流していると、ユーザーが Safari や設定アプリを少し開いて戻ってきた瞬間にレスポンスが止まる、という相談を何度か受けました。私自身、運営している壁紙系の iOS アプリで Gemini を使った「AI による壁紙キャプション生成」を組み込んだとき、TestFlight 配布直後にこの症状で半日溶かしたことがあります。AdMob のリワード動画を見終わって戻ってきた瞬間に切れる、というユーザー報告で気づきました。

2014年から個人開発で App Store にアプリを出し続けている立場から見ると、これは「Gemini 側のエラー」ではなく「iOS の URLSession 設計を理解せずに HTTP ストリーミングを乗せたとき」に起きる典型的な落とし穴です。本来は再接続できるはずなのに、切断検知のロジックがないまま続きを待ち続けて、UI 上は「考え中…」のスピナーが永遠に回るだけになってしまう、というパターンが多いと感じます。

症状 — どんな時に「切れる」のか

ユーザー操作の流れと、観測される挙動を整理しておきます。

  • ストリーミング応答の途中(最初のチャンクは UI に届いている)でアプリをバックグラウンドへ送る
  • 数秒〜30 秒ほど経ってフォアグラウンドに戻す
  • 続きのチャンクがいつまで経っても届かない/ローディングが止まらない
  • Xcode のコンソールには NSURLErrorDomain Code=-1005 (The network connection was lost) が遅れて流れてくる

シミュレータでは再現せず、実機(特に省電力モードや Low Power Mode)で頻発するのも特徴です。私の AdMob 案件のように「リワード動画 → 復帰 → 続きを期待」のような UX を組んでいる場合、ここで折れるとユーザーは黙って離脱します。

まず疑うべきは「アイドルタイムアウト」と「シングルタスク扱い」

iOS の URLSession は、HTTP/1.1 や HTTP/2 のロングコネクション(ストリーミング)を 1 つのタスクとして扱います。アプリがバックグラウンドに入ると、システムは以下のいずれかを判断します。

  • フォアグラウンド限定のセッション(既定の URLSession.shared)は数秒〜十数秒でソケットを閉じる
  • アプリが「Background Modes」未設定なら、URLSessionDataTask は基本的に suspend 対象

つまり「Gemini からまだチャンクが来ていない=アイドル」と OS が判断した瞬間、TCP コネクション自体が落とされます。復帰しても、URLSession が自動でリトライしてくれることはありません。アプリ側で切断検知して再接続のリクエストを投げ直す必要があります。

再現コードと、まずやるべき最小修正

ありがちな「壊れた」実装はこんな形です。

// ❌ よくある実装:背景に入ると簡単に切れる
let url = URL(string: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?key=\(apiKey)")!
var req = URLRequest(url: url)
req.httpMethod = "POST"
req.httpBody = try JSONEncoder().encode(body)
let (bytes, _) = try await URLSession.shared.bytes(for: req)
for try await line in bytes.lines {
    update(line)
}

URLSession.shared は便利ですが、タイムアウトもコネクションも自分でコントロールできません。最低限、専用の URLSession を持ち、ストリーミング用に長めのアイドルタイムアウトと「セルラー使用許可」「待機可」設定を明示します。

// ✅ ストリーミング用にチューニングした URLSession
private let streamingSession: URLSession = {
    let config = URLSessionConfiguration.default
    config.timeoutIntervalForRequest = 60      // 1 チャンク待ちの上限
    config.timeoutIntervalForResource = 300    // ストリーム全体の上限
    config.waitsForConnectivity = true         // 一時的な切断なら自動待機
    config.allowsCellularAccess = true
    config.httpMaximumConnectionsPerHost = 4
    return URLSession(configuration: config)
}()

waitsForConnectivity = true は、Wi-Fi から 4G へ切り替わるような短時間の切断であれば、URLSession 側で接続を待ち合わせてくれます。これだけで「電車のトンネル」系の切断はかなり減ります。

バックグラウンド復帰を検知して、続きを取り直す

ただし、waitsForConnectivity は「ネットワークが復旧したら同じリクエストを送り直す」わけではありません。Gemini の streamGenerateContent は途中再開(resumable streaming)に対応していないので、アプリ側で「どこまで読めたか」を覚えておき、復帰時に新しいリクエストとして投げ直すのが現実解です。

SwiftUI なら ScenePhase を使います。

struct ChatView: View {
    @Environment(\.scenePhase) private var scenePhase
    @StateObject private var vm = ChatViewModel()
 
    var body: some View {
        ChatBody(vm: vm)
            .onChange(of: scenePhase) { _, phase in
                switch phase {
                case .background:
                    vm.snapshotPartial()       // 受信済みテキストを保持
                case .active:
                    if vm.wasInterrupted {
                        Task { await vm.resumeFromSnapshot() }
                    }
                default: break
                }
            }
    }
}

ChatViewModel 側は「ここまで受け取った文字列」を保持しておき、復帰時にはそれをプロンプトに含めて続きを依頼します。Gemini への再リクエストは、ユーザーへの応答を assistant ロール側のメッセージとして履歴に追加し、「直前の応答の続きから書いてください」と指示するだけで、自然な続きが返ってきます。

func resumeFromSnapshot() async {
    var history = currentHistory
    history.append(.init(role: "model", parts: [.text(partialText)]))
    history.append(.init(role: "user", parts: [.text("直前の回答の続きから、同じトーンで書き続けてください。")]))
    await streamGenerate(contents: history)
}

ここでのコツは「同じトーンで」と一言入れることです。これがないと、続きが急に文体だけ変わって不自然になります。

それでも切れる時に確認する 4 つのポイント

上の修正でほとんどのケースは解消しますが、稀に残る症状もあります。確認順に並べておきます。

  1. App Transport Security(ATS)の例外を入れていないか: 独自プロキシ経由にしているとバックグラウンド復帰時に検証が走り、-1200 系のエラーで切れることがあります。素直に Google のエンドポイントを直叩きするのが安定します。
  2. URLSessionDataDelegate を使っているか: SwiftConcurrency の bytes(for:) は手軽ですが、デリゲートを実装した方が didReceive datadidCompleteWithError を取りやすく、デバッグもしやすいです。本番運用ではデリゲート版に倒した方が問題切り分けが早くなります。
  3. VPN や仮想ネットワークが間に入っていないか: 一部の VPN クライアントはアプリのバックグラウンド復帰時にトンネルを張り直すため、その間の HTTP がほぼ確実に切れます。ユーザーから報告が来た時は「VPN を切って試してください」を最初に聞きます。
  4. iOS の Low Power Mode が ON か: 低電力モードでは、バックグラウンド時のネットワーク維持がより積極的に切られます。ProcessInfo.processInfo.isLowPowerModeEnabled で検知し、UI に「省電力モードでは長文生成が中断されることがあります」と一言出すだけでサポート問い合わせが減りました。

私が運用している壁紙アプリでの落とし所

私の場合、累計 5,000 万 DL を超える壁紙アプリ群で、AdMob のリワード動画と AI 生成キャプションを組み合わせるフローを動かしています。「動画を見て戻ってきたらキャプションが完成している」が理想ですが、実際には上記の症状で 1〜2 割が途中で止まっていました。

最終的には、5 秒で完了する短いプロンプトのときだけストリーミングを使い、長いプロンプトは非ストリーミング(generateContent)にフォールバックする実装に落ち着きました。ストリーミングはあくまで「タイピング感覚を出す UI 演出」と割り切り、安定性が必要な場面は完了 API に寄せる、という棲み分けです。

次のアクション

まずは URLSessionConfiguration の 3 行(タイムアウト・waitsForConnectivity・専用セッション化)から手を入れてみてください。ここだけで切断率が半分以下になるはずです。そこから ScenePhase 連動の再開ロジックを足していけば、ユーザーがアプリを切り替えても会話が続いていく iOS らしい体験に近づきます。お読みいただきありがとうございました。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API / SDK2026-08-23
Gemini TTS を streamGenerateContent で受け取り、先頭のチャンクから鳴らす
Gemini の TTS をストリーミングで受け取ると、音声は「ファイル」ではなく PCM の断片で届きます。チャンクごとに WAV ヘッダを付けた場合に何が起きるかを手元で測り、連結とヘッダの正しい順序を実装まで示します。
API / SDK2026-08-07
ストリーミング応答の日本語が静かに欠ける — バイト境界とイベント境界を実測で切り分ける
ストリーミング表示で日本語だけが静かに欠ける現象を、疑似 SSE サーバで実測して切り分けました。UTF-8 のバイト境界とイベント境界という二つの層の壊れ方、英語テストでは検出できない理由、CI に置く意地悪なサーバまで、再現手順つきで整理しています。
API / SDK2026-07-17
途中で切れた Gemini のストリーミングを、引き直すか続きから書かせるか — 二重に払わないための分岐設計
モバイル回線で Gemini のストリーミングが切れたとき、全部引き直すか続きから書かせるかで費用は大きく変わります。思考トークンの二重発生、受信トークン数で置く分岐条件、文末チェックポイント、継ぎ目に出る3つの壊れ方まで設計としてまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →