-
gRPCとSwiftによるリアルタイムのアプリやサービスの構築
gRPCを使用して、Swiftアプリやバックエンドで魅力的なリアルタイムの体験を構築しましょう。gRPCは、高パフォーマンスの双方向ストリーミングAPI向けに設計されたオープンソースのRPCフレームワークです。Swiftの並行処理で構築された最新型の安全なランタイムを提供する、gRPC Swiftパッケージについて解説します。ワークフローを効率化し、リアルタイム機能を簡単に提供できるようにする、統合されたツールも紹介します。
関連する章
- 0:00 - Introduction
- 1:39 - Meet gRPC
- 2:13 - App overview and demo setup
- 3:30 - Defining the ListRaces RPC
- 4:30 - Setting up Xcode to generate gRPC code
- 7:50 - Managing the gRPC client lifecycle
- 9:36 - Protobuf message format and binary efficiency
- 12:33 - Implementing a bidirectional streaming RPC
- 20:11 - Deploying the service
- 23:11 - Next steps
リソース
- About gRPC
- gRPC Swift Extras
- gRPC Swift Protobuf
- gRPC Swift NIO Transport
- gRPC Swift
- Swift on Server
関連ビデオ
WWDC25
WWDC24
WWDC23
-
このビデオを検索
こんにちは Swift Serverチームの Georgeです。 このビデオでは リアルタイム体験を 構築する方法を紹介します gRPC Swiftを使ったアプリや サービスでの実現方法です。 動的なアプリ体験は通常 サーバからのデータ取得に 依存しています ただしサービスの利用は 難しい場合があります。 サービスとやりとりする ネットワークコードを 手書きすることは 時間がかかります。 まずドキュメントから始め 優れたAPIの作成に 時間を費やし 一見うまくいくものが できあがります。 しかしドキュメントが 常に最新とは限らず 途中でミスが 生じることもあるため 期待通りに動作しない 結果になりかねません。 幸い より良い方法があります。 多くのサービスAPIは 仕様書に別途定義されており サービスの信頼できる 情報源となります。 これにより サービスとの やりとりに必要なコードを生成でき 時間を節約しエラーも 排除できます。 この利点は関わるすべての APIにスケールします。 HTTPベースのAPIに 最適な選択肢がOpenAPIです。 広く使われており Swiftでも優れたサポートがあります 同僚の Siが紹介しました "Meet Swift OpenAPI Generator" というセッションで。 次に gRPCという 代替手段を見ていきます。 アプリでシンプルなリクエストを 行う方法を紹介します ストリーミング RPCを使った リアルタイム体験も構築します。 次に gRPCサービスの 実装方法を見ていきます クラウドへの デプロイも行います。 まず gRPCとは何かを 説明します。
gRPCはリモートプロシージャ コール用のフレームワークです。 CNCFプロジェクトで業界で 広く採用された標準規格です。 OpenAPIと同様に仕様から 生成されたコードを使います サービスをすばやく 使い始めることができます。 gRPCではAPIを関数として 入出力で定義します HTTPとしてでは ありません。
実際の動作を 見てみましょう。 近くで新しいゴーカートリーグが 始まります。 すべてを追跡する システムがあります スケジュールから ライブレースデータまで管理します ただしその情報を公開する 方法が必要です。 iOSアプリを統合する 作業を進めています gRPCサービスを介した バックエンドとの統合です。 アプリにいくつかのビューを 用意しました サンプルデータを 入力してあります。 予定のレースを 一覧表示できます。 各レースをタップすると 詳細情報が確認できます。 gRPCを使ってサーバから コンテンツを取得できると良いです。 レーススケジュールを返す関数は list racesと呼べるでしょう。 要求するレース数を 引数として呼び出せます レースのリストを 返します。 リモートプロシージャコールとして クライアントからサーバへ リクエストメッセージが送信され 関数が実行されます レースのリストが レスポンスとして返されます。 この理論を実践してみましょう アプリで gRPC Swiftを使う 方法を見ていきます。
まずサービスAPIの 定義から始めます。 次に Xcodeプロジェクトに 必要な依存関係を追加します gRPCビルドプラグインを設定して サービス呼び出しコードを生成します。 アプリを更新して サーバを呼び出します。
gRPCサービスを指定する最も 一般的な形式はProtocol Buffersです 略して Protobufとも呼びます。
.protoファイルで ListRacesという RPCを1つ持つサービスを定義します。 入力として ListRacesRequestを 出力として ListRacesResponseを 持ちます。 リクエストメッセージには limitというフィールドがあります レースの最大数を表す 整数値です レスポンスに含める数を 指定します。 デフォルト値は100に 設定されています。 メッセージの各フィールドには 固有のフィールド番号が割り当てられます。 レスポンスメッセージには Raceフィールドが繰り返し含まれます。 Raceは別のメッセージとして定義され 名前などの情報を含みます ロケーションやチャンピオンシップは 文字列 ラップ数は整数です 開始時刻は タイムスタンプです。 タイムスタンプ型はProtobufの Well Known Typesのひとつです 他の場所で定義されているため インポートが必要です。
サービスを定義したので 次に切り替えます Xcodeで gRPC依存関係を 追加します コードジェネレーターを 設定します。
始めるにあたり プロジェクトに いくつかの依存関係を追加します。 Project Editorに 移動します。
タブを 選択します をクリックします。
まず grpc-swift-nio-transportの 依存関係を追加します 高パフォーマンスな ネットワークコードを提供します オープンソースの SwiftNIOライブラリ 上に構築されています。
次に依存関係を追加します grpc-swift-protobufには ビルドプラグインが含まれています protoファイルから gRPCコードを 生成するためのものです。
依存関係を設定したので ターゲットでビルドプラグインを 使用するよう設定します。 appターゲットを選択します。 タブを 選択します セクションを展開します。 アイコンをクリックして GRPCProtobufGeneratorを選択し をクリックします。
プラグインはターゲットディレクトリの protoファイルをスキャンします JSONの設定ファイルで 構成できます。 今からターゲットに 追加します。
JSONファイルは生成される コードを設定します。 アプリのためメッセージと クライアントのみ必要です サーバコードは不要です。 アプリを再コンパイルして コードを生成します。 セキュリティ上の理由から 初回使用時にプラグインを 信頼するよう求められます。
設定が完了し サービスを呼び出す準備ができました。 RaceScheduleViewを 開きます。
必要なモジュールを インポートします。
coreモジュールは共通の gRPC ランタイムコンポーネントを提供します HTTPモジュールは ネットワークコードを提供します SwiftProtobufでProtobufメッセージを 操作できます。 次にビューにtaskモディファイアを 追加します リクエストを行います。 タスク内で withGRPCClient関数を使って クライアントを作成します。 do-catchブロック内で行い 今はエラーを出力します。 gRPC Swiftではネットワーク実装を 設定できます。 SwiftNIOベースのものを使って Mac上のローカルサーバに接続します。
クロージャに渡されるクライアントは サーバのことだけを把握しています サービスについては 何も知りません。 ここで生成されたコードが 役立ちます。 SwiftKartクライアントを 作成します
gRPCクライアントで 初期化します
リクエストを作成し
list races RPCを呼び出して レスポンスを待ちます。
最後に新しいデータで ビューを更新します サーバからのレスポンスを変換して ビューで使用するデータモデルに マッピングします。
これでローカルサーバから レーススケジュールを取得できました。 Finite Loopsは楽しそうです もうすぐ始まります!
先に進む前に 重要な変更が必要です。
現在 アプリはビューが表示されるたびに 新しい gRPCクライアントを作成します。 各ビューがサーバへの接続を 個別に確立する必要があります 不要なレイテンシが 発生します。 代わりにアプリはクライアントを1つ作成して ビュー間で共有すべきです 接続を再利用できるようにするためです。
アプリの環境を通じて クライアントを伝播できます。 クライアントは切断すべきです アプリがバックグラウンドに入ったとき リソースを解放するためです。 Xcodeで 事前に作成した クライアントマネージャーのコードを追加します。
アプリのエントリーポイントを開き マネージャーのインスタンスを作成します。
environmentモディファイアで 子ビューから利用できるようにします。 シーンがバックグラウンドに入ったとき クライアントを切断すべきです。 そのためにscene phaseプロパティを 作成します
変更を監視します。
マネージャークラスは遅延接続します クライアントを要求されたとき接続するため 特別な処理は不要です シーンがアクティブになる際の 処理は不要です。 RaceScheduleViewで マネージャーを使います。 ビューに追加します
プレビューでも利用可能にします。
最後にwithGRPCClientの呼び出しを マネージャーへの呼び出しに置き換えます。
アプリのセットアップと サービスとの通信が完了しました Protobufで定義されたサービスAPIから 生成されたコードを使っています。
サービスAPIに加えて Protobufはメッセージ交換 フォーマットを提供します。 SwiftProtobufにはコードジェネレーターがあります Swiftの型を直接扱えます メッセージを表す型を使えます。 例えばraceメッセージを作成し フィールドに関連情報を 入力できます。 gRPCがクライアントとサーバ間で メッセージを送受信するとき バイナリ形式に シリアライズします。 フィールドの識別には名前ではなく 固有のフィールド番号を使用します。 その結果 Protobufメッセージは 同等のJSONメッセージの 約半分のサイズになります。 メッセージサイズの削減は モバイルアプリに最適です データ転送量を最小化することで ネットワーク呼び出しの パフォーマンスが向上します。 ネットワーク環境が悪い場合に 特に重要です。 この効率性は他の環境でも優れています サービス間通信などでも活躍します。 プロセス間通信にも有用です Appleのオープンソースの Containerizationフレームワークなどで活用されています。 gRPC Swiftを使って 仮想ソケット越しに通信します ホストOSとLinux仮想マシン間の 通信に使います。 gRPC Swiftはクラウドサービスの 主要コンポーネントでもあります Private Cloud Computeなどで使われており iCloudキーチェーンや写真とSharePlayの ファイル共有にも活用されています。 ただし用途は 外部向けサービスだけではなく gRPCは社内インフラの 深部でも稼動しています OSのビルドやリリースシステムなどでも 使われています。 gRPCの優れた機能のひとつが ストリーミングのファーストクラスサポートです list racesなど多くのRPCは 単一のリクエストメッセージを サーバに送信し 単一のレスポンスメッセージが 返ってきます。 これをユニナリーRPCと呼びます。 ただしRPCはリクエストと レスポンスをストリーム配信できます 探索できる他のRPCの種類が 3つあります。 クライアントストリーミングRPCは クライアントが何件でもメッセージをサーバに送信し サーバは単一のレスポンスを 返すタイプです。 各ゴーカートがテレメトリーデータを サーバにストリーミングするイメージです。 サーバストリーミングRPCはクライアントが 単一のリクエストをサーバに送信し サーバが何件でもレスポンスを 返すタイプです。 ライブテキスト実況フィードのような リアルタイム更新をイメージしてください。 最後の種類は 双方向ストリーミングです クライアントとサーバが 互いに何件でもメッセージを送れます。 アプリでこれを活用する 良いアイデアがあります ライブレース更新の提供に使います。 リクエストメッセージはサーバに伝えます クライアントがどのイベントを サブスクライブしたかを レスポンスメッセージには 該当するイベントが含まれます。 クライアントは変更に応じて さらにメッセージをサーバに送れます 受信したいイベントが 変わった場合に送ります。
アプリはMac上で動作する サーバへリクエストを送っています そのサーバもSwiftで 書かれています。 見てみましょう。
サーバのセットアップは 簡単です。 サーバオブジェクトを作成し トランスポートで初期化します 提供するサービスも指定します。 サーバを起動するには serveを呼び出すだけです。 サービスはひとつの型で ビルドプラグインが生成した プロトコルを実装します。 先ほど実装した list races RPCを 確認できます。 async関数で リクエストを受け取り レスポンスを返します。 実装はデータベースからレースを クエリするだけです メッセージに値を設定して 返します。 ストリーミングRPCを組み込むため サービス定義を更新します 次にサーバに切り替えて コードを再生成します 新しいRPCを実装するためです。 完了したらアプリを更新して 呼び出します。 まずFollowRace RPCを サービス定義に追加します。 RPCがリクエストとレスポンスを ストリーム配信するため 入力と出力の前に streamキーワードを追加します。 次にメッセージを定義します。 リクエストメッセージには 追跡するレースの名前が含まれます サブスクライブするイベントタイプの リストも含まれます enumとして表現されます。 レスポンス型はoneofフィールドを持ちます 関連値を持つSwift enumと 同様です。 メッセージは各カートの位置情報か 現在のレース順位を保持します それぞれ別のメッセージとして定義されています。 サービス定義が更新されたので Xcodeでサーバに切り替えます 新しいRPCの実装に取り組みます。 プロジェクトをビルドして コードを再生成します。
プロトコルに新しい要件が追加されたため ビルドエラーが発生しています まだ実装していないので スタブを作成します。
ストリーミングにより list races RPCとは 少し異なる形になっています。 リクエストパラメーターはリクエストメッセージの async sequenceです レスポンスパラメーターはオブジェクトで クライアントへのレスポンスメッセージを 書き込むためのものです。 2つのデータストリームを 同時に処理する必要があるため タスクグループが必要です。
最初のリクエストメッセージを 待ちます 追跡するレースの名前を 確認するためです 呼び出し元が関心を持つ イベントも確認します。 async iteratorを作成して 最初のメッセージを待ちます。 mutexで保護されたセットに イベントを保存します 2つの異なるタスクが 並行してアクセスするためです。 タスクグループにタスクを追加します 追跡するレース名を指定して ライブレーストラッカーを呼び出します。
イベントのasync sequenceが得られ フィルタリングできます クライアントが現在関心を持つ ものだけに絞ります。
フィルタリングされたイベントを 反復します
空のレスポンスメッセージを 作成します。
イベントでswitchして メッセージに値を設定します。 まずトラッカーからのカート位置の 配列を変換します RPCで使用するデータ型に変換します。 順位についても同様に処理します。
クライアントにメッセージを 書き込みます。
あといくつか対応が必要です。 まずリクエストメッセージを 引き続き受信します 呼び出し元が関心のあるイベントを 変更する可能性があるためです。 リクエストストリームの終端を使って クライアントがイベント受信を終了した ことを示すシグナルとします タスクグループで実行中の タスクをキャンセルします メッセージの送信を停止するためです。 最後にサーバを再起動して クライアントが新しいRPCを呼び出せるようにします。
サービスのRPC実装が完了し アプリを更新できます。 アプリのXcodeプロジェクトを開き 新しいRPCとメッセージを含めて protoファイルを更新します。
プロジェクトをビルドして gRPCコードを再生成します。
RaceInfoViewに移動します。 事前に作成したLiveStreamViewへの NavigationLinkを追加します。 ライブストリームビューを開きます。
マップを表示して アノテーションを描画します レース中の各カートの 位置を表示します。 ツールバーのボタンでシートを開きます ライブリーダーボードを表示します。 showLeaderboardプロパティで 表示状態を追跡します。 ビューにはすでにプロパティがあります 関心のある各種状態を 保存するためのプロパティです あとはRPCを呼び出して サーバから受信したデータを 接続するだけです。 まず先ほど使ったインポートを追加します。 次に環境を通じて クライアントを注入します。
以前と同様にタスクを作成します
manager.withClientを呼び出します。
カートクライアントを作成し
FollowRace RPCを呼び出します。
ユニナリーの list races RPCとは 構造が異なります。 2つのクロージャを持ちます ひとつはリクエストメッセージの書き込み用 もうひとつはレスポンスメッセージの 処理用です。 showLeaderboardの値が変わるたびに リクエストメッセージを送信します。 AsyncStreamを使って 時間経過を追跡します continuationをプロパティとして保存します。
showLeaderboardが変化したとき continuationに新しい値を yieldします。
タスク内でAsyncStreamと continuationを作成します。
showLeaderboardの現在の値を ストリームにyieldします 初期値として設定します。 RPCの最初のクロージャで ストリームを反復します
各値についてサーバに メッセージを送信します。
リーダーボードが表示されている場合 standingsイベントを追加します。
サーバにメッセージを 書き込みます。
レスポンスクロージャ内で メッセージを反復して各イベントの ビュー状態を更新します。 イベント処理には ヘルパーメソッドを使います。
イベントでswitchして 各イベントをビューで使う データ型に変換します。
カートの位置情報から始めます。 次に順位についても 同様に処理します
最後にレスポンスメッセージを 反復します 各イベントのヘルパーを呼び出します。
確認してみましょう。
Apple Parkでレースが 始まりそうです。 Rainbow Archesに 向かっています Duck Pondに向かって 右折しているようです。 Montyがリードしておりその後ろに PepperとBoが続いています。 素晴らしいですが まだ目標を達成できていません 観戦者に情報を公開するという目標です サービスがローカルで動作しているためです。
クラウドにデプロイして アプリを使う全員が 利用できるようにします。 Google Cloud Platformを使って サービスをホストしています AWSやFly.ioなど別のプラットフォームも 使えます。 アプローチは似ていますが 具体的な手順は異なります。 ほとんどのサーバはLinuxで動作しており 今日はLinuxにデプロイします。 コードの変更は不要ですが サーバの実行ファイルを パッケージ化する必要があります ランタイム依存関係と一緒に コンテナイメージにします。 次にクラウドプロバイダの イメージレジストリに公開します。 その後デプロイメントを作成します。 最後にデプロイしたサービスを対象に アプリを更新します。 まずContainerfileを作成します コンテナイメージのビルドに必要な 手順を記述します。 ベースイメージにswift:latestを使います。 次に作業ディレクトリを設定し パッケージマニフェストと ソースファイルをコピーします。 サーバをリリースモードでビルドして 既知の場所にコピーします。 この時点でイメージにサーバの 実行ファイルがあります ただしSwiftツールチェーン全体も 含まれています。 サーバの実行にそれらはすべて不要です イメージが必要以上に 大きくなってしまいます。
マルチステージビルドを使って バイナリをswift:slimランタイムイメージに コピーします。 最後にポートを公開してエントリーポイントを サーバに設定します。
Containerfileが完成しました ここでイメージをビルドして公開します コンテナレジストリに 数分かかるため 事前に公開したイメージを使います。 ターミナルでgcloud run deployコマンドを 使えます。
デプロイメント名 イメージ名 リージョンを指定します。 サービスがhttp2を使用することを 指定します 未認証リクエストを許可します。
デプロイが完了すると サービスのURLが表示されます クライアント更新時に必要なので 今コピーします。
アプリに切り替えて ClientManagerを開きます。 接続ターゲットをサービスの DNS名に更新します デプロイコマンドからのDNS名です。 トランスポートセキュリティオプションを 変更してTLSを有効にします plaintextからTLSに変更します。
テストしてみましょう。
Finite Loopsレースのスタートに 間に合いました。
Pepperにとって最悪のスタートです Montyがトップに立ち MycroftとKikoが続いています。
ドライバーたちがInfinite Loopキャンパスに 進入しています。
Pepperが数つ順位を上げました。
素晴らしいレースになりそうです。
gRPC Swiftの使い方をご紹介しました アプリに優れたライブ体験を 構築するための方法です アプリとサーバ間の通信を 簡素化する方法もご紹介しました サービスの定義からコードの生成まで クラウドへのサービス実装と デプロイまで一貫しています。 これはまだ始まりにすぎません。 gRPC Swiftには 多くの組み込み機能があります アプリをプロトタイプから 本番環境へと導きます 他のSwiftパッケージとの 統合など Swift OTelや Swift service lifecycleなどと統合できます。 カスタムトランスポートやネームリゾルバーなど 高度な接続管理機能もあります クライアントサイドの ロードバランシングもあります。 アプリでgRPCを使う 準備ができました。 アプリとサーバ間のやりとりの 一部をプロトタイプしてみては gRPC Swiftがワークフローをどれだけ シンプルにするかを体験してください。 またはGitHubで公開されている プロジェクトのリポジトリにある チュートリアルやサンプルを試してみてください。 プロジェクトはオープンソースなので コントリビュートもできます 質問したり ドキュメントを改善したり 新機能を提案・実装したりできます。 ご視聴ありがとうございました コースでお会いしましょう!
-
-
3:38 - ListRaces RPC definition
edition = "2024"; import "google/protobuf/timestamp.proto"; service SwiftKartService { rpc ListRaces(ListRacesRequest) returns (ListRacesResponse); } message ListRacesRequest { int32 limit = 1 [default = 100]; } message ListRacesResponse { repeated Race races = 1; } message Race { string name = 1; string location = 2; google.protobuf.Timestamp start_time = 3; int32 laps = 4; string championship = 5; } -
5:55 - grpc-swift-proto-generator-config.json
{ "generate": { "clients": true, "servers": false, "messages": true } } -
6:24 - Add gRPC imports
import GRPCCore import GRPCNIOTransportHTTP2 import SwiftProtobuf -
6:38 - Create a gRPC client connected to a local server
.task { do { try await withGRPCClient( transport: .http2NIOTS( address: .ipv4(host: "127.0.0.1", port: 8080), transportSecurity: .tls ) ) { client in <#code#> } } catch { print("gRPC error: \(error)") } } -
7:14 - Call the ListRaces RPC and update the view
.task { do { try await withGRPCClient( transport: .http2NIOTS( address: .ipv4(host: "127.0.0.1", port: 8080), transportSecurity: .tls ) ) { client in let kart = SwiftKartService.Client(wrapping: client) let request = ListRacesRequest() let response = try await kart.listRaces(request) self.races = response.races.map { race in RaceInfo( name: race.name, location: race.location, startTime: race.startTime.date, championship: race.championship, laps: Int(race.laps), drivers: race.drivers ) } } } catch { print("gRPC error: \(error)") } } -
8:30 - ClientManager.swift
import GRPCCore import GRPCNIOTransportHTTP2 import Synchronization import SwiftUI @Observable final class ClientManager: Sendable { fileprivate let state = Mutex(State.disconnected) static func makeTransport() throws -> HTTP2ClientTransport.TransportServices { try .http2NIOTS( target: .ipv4(address: "127.0.0.1", port: 8080), transportSecurity: .plaintext ) } func withClient( body: (_ client: GRPCClient<HTTP2ClientTransport.TransportServices>) async throws -> Void ) async throws { let client = try connectIfNecessary() try await body(client) } private func connectIfNecessary() throws -> GRPCClient<HTTP2ClientTransport.TransportServices> { try self.state.withLock { state in try state.connectIfNecessary() } } func disconnect() { let client = self.state.withLock { state in state.disconnect() } client?.beginGracefulShutdown() } } extension ClientManager { enum State { case connected(GRPCClient<HTTP2ClientTransport.TransportServices>, Task<Void, any Error>) case disconnected } } extension ClientManager.State { mutating func connectIfNecessary() throws -> GRPCClient<HTTP2ClientTransport.TransportServices> { switch self { case .connected(let client, _): return client case .disconnected: let client = try GRPCClient(transport: ClientManager.makeTransport()) let task = Task { try await client.runConnections() } self = .connected(client, task) return client } } mutating func disconnect() -> GRPCClient<HTTP2ClientTransport.TransportServices>? { switch self { case .connected(let client, _): self = .disconnected return client case .disconnected: return nil } } } -
8:39 - Propagate ClientManager to child views
import SwiftUI @main struct SwiftKartApp: App { let manager = ClientManager() var body: some Scene { WindowGroup { RaceScheduleView() .environment(manager) } } } -
8:52 - Disconnect ClientManager when the scene enters the background phase
import SwiftUI @main struct SwiftKartApp: App { let manager = ClientManager() @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { RaceScheduleView() .environment(manager) } .onChange(of: scenePhase) { _, newPhase in switch newPhase { case .background : manager.disconnect() case .inactive, .active: break @unknown default: break } } } } -
9:12 - Inject ClientManager into the view via @Environment
@Environment(ClientManager.self) var manager -
9:21 - Replace withGRPCClient with manager.withClient
.task { do { try await manager.withClient { client in let kart = SwiftKartService.Client(wrapping: client) let request = ListRacesRequest() let response = try await kart.listRaces(request) self.races = response.races.map { race in RaceInfo( name: race.name, location: race.location, startTime: race.startTime.date, championship: race.championship, laps: Int(race.laps), drivers: race.drivers ) } } } catch { print("gRPC error: \(error)") } } -
9:41 - Using SwiftProtobuf
var race = Race() race.name = "Duck Pond Dash" race.location = "Apple Park, Cupertino" race.startTime = .init(roundingTimeIntervalSince1970: 1_781_198_600) race.laps = 6 race.championship = "Corporate Cup" race.drivers = ["Monty", "Pepper", "Mycroft", "Pancakes", "Duke", "Kiko", "Sissi", "Bo"] try race.serializedBytes() -
12:32 - Server
let server = GRPCServer( transport: .http2NIOPosix( address: .ipv4(host: "127.0.0.1", port: 8080), transportSecurity: .plaintext ), services: [Service()] ) try await server.serve() -
12:45 - Service
struct Service: SwiftKartService.SimpleServiceProtocol { private let database = RaceDB() func listRaces( request: ListRacesRequest, context: ServerContext ) async throws -> ListRacesResponse { var response = ListRacesResponse() response.races = await database.listRaces(atMost: request.limit) return response } } -
13:20 - swift_kart_service.proto
edition = "2024"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; service SwiftKartService { rpc ListRaces(ListRacesRequest) returns (ListRacesResponse); rpc FollowRace(stream FollowRaceRequest) returns (stream FollowRaceResponse); } message ListRacesRequest { int32 limit = 1 [default = 100]; } message ListRacesResponse { repeated Race races = 1; } message Race { string name = 1; string location = 2; google.protobuf.Timestamp start_time = 3; int32 laps = 4; string championship = 5; repeated string drivers = 6; } message FollowRaceRequest { string race_name = 1; repeated RaceEventType event_types = 2; } enum RaceEventType { RACE_EVENT_TYPE_UNSPECIFIED = 0; RACE_EVENT_TYPE_KART_LOCATIONS = 1; RACE_EVENT_TYPE_STANDINGS = 2; } message FollowRaceResponse { oneof event { KartLocations locations = 1; Standings standings = 2; } } message KartLocations { message Kart { int32 number = 1; double latitude = 2; double longitude = 3; google.protobuf.Timestamp recorded_at = 4; } repeated Kart karts = 1; } message Standings { message Entry { int32 kart_number = 1; google.protobuf.Duration gap_to_leader = 2; int32 position = 3; int32 lap = 4; } repeated Entry entries = 1; } -
14:16 - FollowRace stub
func followRace( request: RPCAsyncSequence<FollowRaceRequest, any Error>, response: RPCWriter<FollowRaceResponse>, context: ServerContext ) async throws { throw RPCError(code: .unimplemented, message: "FollowRace is unimplemented") } -
14:38 - Implement the FollowRace RPC
func followRace( request: RPCAsyncSequence<FollowRaceRequest, any Error>, response: RPCWriter<FollowRaceResponse>, context: ServerContext ) async throws { try await withThrowingTaskGroup { group in var iterator = request.makeAsyncIterator() guard let first = try await iterator.next() else { return } let eventTypes = Mutex(Set(first.eventTypes)) group.addTask { let events = tracker.events(forRace: first.raceName).filter { event in eventTypes.withLock { $0.contains(event.type) } } for await event in events { var message = FollowRaceResponse() switch event { case .locations(let locations): message.locations.karts = locations.map { location in var kart = KartLocations.Kart() kart.number = Int32(location.number) kart.latitude = location.latitude kart.longitude = location.longitude return kart } case .standings(let standings): message.standings.entries = standings.map { standing in var entry = Standings.Entry() entry.gapToLeader = .init(rounding: standing.delta, rule: .towardZero) entry.kartNumber = Int32(standing.kartNumber) entry.lap = Int32(standing.lap) entry.position = Int32(standing.position) return entry } } try await response.write(message) } } while let next = try await iterator.next() { eventTypes.withLock { $0 = Set(next.eventTypes) } } group.cancelAll() } } -
16:39 - swift_kart_service.proto
edition = "2024"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; service SwiftKartService { rpc ListRaces(ListRacesRequest) returns (ListRacesResponse); rpc FollowRace(stream FollowRaceRequest) returns (stream FollowRaceResponse); } message ListRacesRequest { int32 limit = 1 [default = 100]; } message ListRacesResponse { repeated Race races = 1; } message Race { string name = 1; string location = 2; google.protobuf.Timestamp start_time = 3; int32 laps = 4; string championship = 5; repeated string drivers = 6; } message FollowRaceRequest { string race_name = 1; repeated RaceEventType event_types = 2; } enum RaceEventType { RACE_EVENT_TYPE_UNSPECIFIED = 0; RACE_EVENT_TYPE_KART_LOCATIONS = 1; RACE_EVENT_TYPE_STANDINGS = 2; } message FollowRaceResponse { oneof event { KartLocations locations = 1; Standings standings = 2; } } message KartLocations { message Kart { int32 number = 1; double latitude = 2; double longitude = 3; google.protobuf.Timestamp recorded_at = 4; } repeated Kart karts = 1; } message Standings { message Entry { int32 kart_number = 1; google.protobuf.Duration gap_to_leader = 2; int32 position = 3; int32 lap = 4; } repeated Entry entries = 1; } -
16:40 - swift_kart_service.proto
edition = "2024"; import "google/protobuf/timestamp.proto"; service SwiftKartService { rpc ListRaces(ListRacesRequest) returns (ListRacesResponse); } message ListRacesRequest { int32 limit = 1 [default = 100]; } message ListRacesResponse { repeated Race races = 1; } message Race { string name = 1; string location = 2; google.protobuf.Timestamp start_time = 3; int32 laps = 4; string championship = 5; repeated string drivers = 6; } -
16:56 - Navigation link to LiveStreamView
NavigationLink(destination: LiveStreamView(race: race)) { Text("Live stream") } -
17:32 - Call the FollowRace RPC in the LiveStreamView
import SwiftUI import GRPCCore import GRPCNIOTransportHTTP2 import SwiftProtobuf struct LiveStreamView: View { private let race: RaceInfo @Environment(ClientManager.self) var manager @State private var tracking: KartTrackingViewModel @State private var standings: [StandingsEntry] = [] @State private var showLeaderboard = false @State private var continuation: AsyncStream<Bool>.Continuation? init(race: RaceInfo) { self.race = race self.tracking = KartTrackingViewModel(race: race) } var body: some View { VStack { KartTrackingMapView(viewModel: tracking) .ignoresSafeArea() .onAppear { tracking.start() } .onDisappear { tracking.stop() } } .onChange(of: showLeaderboard) { _, newValue in continuation?.yield(newValue) } .sheet(isPresented: $showLeaderboard) { LeaderboardView(race: race, standings: standings) .presentationDetents([.fraction(0.3), .medium, .large]) .presentationBackgroundInteraction(.enabled) } .toolbar { Toggle(isOn: $showLeaderboard) { Label("Leaderboard", systemImage: "list.number") } } .toolbarBackgroundVisibility(.visible, for: .navigationBar) .task { do { let (stream, continuation) = AsyncStream.makeStream(of: Bool.self) self.continuation = continuation continuation.yield(showLeaderboard) try await manager.withClient { client in let kart = SwiftKartService.Client(wrapping: client) try await kart.followRace { requestStream in for await showLeaderboard in stream { var message = FollowRaceRequest() message.raceName = race.name message.eventTypes = [.kartLocations] if showLeaderboard { message.eventTypes.append(.standings) } try await requestStream.write(message) } } onResponse: { responseStream in for try await message in responseStream.messages { if let event = message.event { await handleEvent(event) } } } } } catch { print("gRPC error: \(error)") } } } @MainActor private func handleEvent(_ event: FollowRaceResponse.OneOf_Event) { switch event { case .locations(let locations): self.tracking.updateKartCoordinates( locations.karts.map { TrackedKart(number: $0.number, latitude: $0.latitude, longitude: $0.longitude) } ) case .standings(let standings): self.standings = standings.entries.map { StandingsEntry( kartNumber: $0.kartNumber, secondsToLeader: $0.gapToLeader.timeInterval, position: $0.position, lap: $0.lap ) } } } } #Preview { NavigationStack { LiveStreamView(race: .example4) .environment(ClientManager()) } } -
20:55 - Containerfile
FROM swift:latest AS builder # Copy sources into /app WORKDIR /app COPY Package.swift Package.resolved . COPY Sources/ Sources/ # Build the server RUN swift build -c release --product server RUN cp "$(swift build -c release --show-bin-path)/server" /usr/bin/server # Copy the binary from the builder into a smaller runtime image. FROM swift:slim COPY --from=builder /usr/bin/server /usr/bin/server EXPOSE 8080 ENTRYPOINT ["/usr/bin/server"] -
21:56 - Deploy service
gcloud run deploy wwdc-demo-server \ --image us-central1-docker.pkg.dev/wwdc26/wwdc-demo-server/wwdc-demo-server:latest \ --region us-central1 \ --use-http2 \ --allow-unauthenticated -
22:22 - Target deployed service
static func makeTransport() throws -> HTTP2ClientTransport.TransportServices { try .http2NIOTS( target: .dns(host: "wwdc-demo-server-863666503339.us-central1.run.app"), transportSecurity: .tls ) }
-
-
- 0:00 - Introduction
Why hand-crafting networking code is error-prone, and how generating code from a service specification saves time and eliminates mistakes — setting up gRPC Swift as the approach for real-time experiences.
- 1:39 - Meet gRPC
gRPC is explained as a CNCF-standard remote procedure call framework that uses Protocol Buffers to define APIs as typed functions rather than HTTP endpoints.
- 2:13 - App overview and demo setup
A go-karting iOS app demo is introduced, showing how gRPC will replace static mock data with live server-fetched content.
- 3:30 - Defining the ListRaces RPC
The ListRaces RPC and its request/response messages are defined in a .proto file, covering fields, field numbers, types, and Protobuf Well Known Types.
- 4:30 - Setting up Xcode to generate gRPC code
The grpc-swift-nio-transport and grpc-swift-protobuf packages are added to the Xcode project, and the GRPCProtobufGenerator build plugin is configured to auto-generate Swift code from the proto file.
- 7:50 - Managing the gRPC client lifecycle
A shared ClientManager is introduced to reuse connections across views and disconnect the client when the app enters the background, reducing unnecessary latency.
- 9:36 - Protobuf message format and binary efficiency
The Protobuf binary serialization format is explained — using field numbers instead of names makes messages roughly half the size of equivalent JSON, benefiting mobile apps and service-to-service communication.
- 12:33 - Implementing a bidirectional streaming RPC
The FollowRace bidirectional streaming RPC is defined, implemented on the Swift server using async sequences and task groups, and wired up in the iOS app to stream live kart positions and standings.
- 20:11 - Deploying the service
The Swift server is containerised and deployed, then the app is updated to connect over TLS to the live production service.
- 23:11 - Next steps
Recap of the full gRPC workflow, with pointers to prototype your own integrations, explore the open-source GitHub repository, and contribute to the project.