View in English

  • Apple 开发者
    • 入门汇总

    探索“入门汇总”

    • 概览
    • 学习
    • Apple Developer Program

    及时了解最新动态

    • 最新动态
    • 开发者你好
    • 平台

    探索“平台”

    • Apple 平台
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    • App Store

    精选

    • 设计
    • 分发
    • 游戏
    • 配件
    • 网页
    • Home
    • CarPlay 车载
    • 技术

    探索“技术”

    • 概览
    • Xcode
    • Swift
    • SwiftUI

    精选

    • 辅助功能
    • App Intents
    • Apple 智能
    • 游戏
    • 机器学习与 AI
    • 安全性
    • Xcode Cloud
    • 社区

    探索“社区”

    • 概览
    • “与 Apple 会面交流”活动
    • 社区主导的活动
    • 开发者论坛
    • 开源

    精选

    • WWDC
    • Swift Student Challenge
    • 开发者故事
    • App Store 大奖
    • Apple 设计大奖
    • Apple Developer Centers
    • 文档

    探索“文档”

    • 文档库
    • 技术概述
    • 示例代码
    • 《人机界面指南》
    • 视频

    发布说明

    • 精选更新
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • Apple tvOS
    • Xcode
    • 下载

    探索“下载”

    • 所有下载
    • 操作系统
    • 应用程序
    • 设计资源

    精选

    • Xcode
    • TestFlight
    • 字体
    • SF Symbols
    • Icon Composer
    • 支持

    探索“支持”

    • 概览
    • 帮助指南
    • 开发者论坛
    • “反馈助理”
    • 联系我们

    精选

    • 《开发者账户帮助》
    • 《App 审核指南》
    • 《App Store Connect 帮助》
    • 即将实行的要求
    • 协议和准则
    • 系统状态
  • 快速链接

    • 活动
    • 新闻
    • 论坛
    • 示例代码
    • 视频
 

视频

打开菜单 关闭菜单
  • 专题
  • 所有视频
  • 关于

更多视频

  • 简介
  • 概要
  • 转写文稿
  • 代码
  • 使用 gRPC 和 Swift 构建实时 App 及服务

    使用 gRPC 在你的 Swift App 中及后端打造引人入胜的实时体验。gRPC 是一个开源的 RPC 框架,专为高性能、双向流式 API 设计。探索 gRPC Swift 软件包如何借助 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

    • 了解 Containerization

    WWDC24

    • 探索 Swift on Server 生态系统

    WWDC23

    • 认识 Swift OpenAPI Generator
  • 搜索此视频…

    你好 我是Swift Server团队的George 在本视频中 我将向你展示如何构建 实时体验 在你的App和服务中使用gRPC Swift 动态App体验通常依赖 从服务器获取数据 但与服务交互可能颇具挑战 手动编写网络代码 与服务交互可能非常耗时 从一些文档开始 花时间设计出色的API 最终得到一些看似可用的东西 但文档并不总是最新的 而且你可能在过程中犯了一些错误 结果可能不总是按预期运行 幸运的是 有更好的方法 许多服务API在规范中单独定义 它作为服务的可信来源 这让你能生成 与之交互所需的代码 节省时间并消除错误 这些优势适用于所有API 你需要交互的API 基于HTTP的API 优选方案是OpenAPI 它被广泛使用 在Swift中也有很好的支持 我的队友Si对此进行了介绍 在名为 "Meet Swift OpenAPI Generator"的专题中 我们来看看称为gRPC的替代方案 我将展示如何在App中使用它 发出简单请求 并使用流式RPC构建实时体验 接着 我们将了解 如何实现gRPC服务 并将其部署到云端 首先 我们来了解gRPC是什么

    gRPC是一个用于 远程过程调用的框架 它是CNCF项目 也是被广泛采用的行业标准 与OpenAPI类似 你使用从规范生成的代码 让你能快速开始使用服务 但在gRPC中 API以 带有输入和输出的函数来定义 而非基于HTTP来定义

    我们来看看实际运作方式 附近有一个新的卡丁车联赛即将开始 他们有一套系统追踪所有信息 从赛程到所有实时比赛数据 但他们需要一种方式 来提供这些信息 我一直在开发一款iOS App 通过gRPC服务与其后端集成 我在App中准备了几个视图 并填充了一些示例数据 可以列出即将到来的比赛 然后点击每条以获取更多信息 但如果能用gRPC 从服务器获取内容就更好了 返回赛程的函数 可称为list races 可以用请求的比赛数量来调用它 它将返回比赛列表 作为远程过程调用 请求消息由客户端发送到服务器 服务器执行函数 并将比赛列表作为响应返回 让我们将理论付诸实践 看看如何在App中使用gRPC Swift

    首先定义服务API 然后向Xcode项目添加所需依赖项 配置gRPC构建插件 生成调用服务所需的代码 然后更新App以调用服务器 指定gRPC服务最常见的格式 称为Protocol Buffers 简称Protobuf

    在.proto文件中 我将定义 一个名为ListRaces的RPC服务 它以ListRacesRequest作为输入 以ListRacesResponse作为输出 请求消息有一个名为limit的字段 它是一个整数 表示比赛数量的最大值 响应中包含的比赛数量 我给它设置了默认值100 消息中的每个字段 也被分配唯一的字段编号 响应消息包含一个重复的Race字段 Race被定义为单独的消息 包含名称等信息 位置和锦标赛名称作为字符串 圈数作为整数 以及开始时间作为时间戳 此类型是Protobuf知名类型 在其他地方定义 因此我需要导入其定义 定义好服务后 我可以切换 到Xcode添加gRPC依赖项 并配置代码生成器

    首先 我需要向项目添加一些依赖项 我将导航到项目编辑器

    然后选择 Package Dependencies选项卡 然后点击加号

    首先 我将添加对 grpc-swift-nio-transport的依赖 它提供高性能网络代码 构建于开源SwiftNIO库之上

    然后我将添加依赖项 对grpc-swift-protobuf 它提供构建插件 用于从proto文件生成gRPC代码

    设置好依赖项后 我可以配置目标以使用构建插件 我将选择App Target 然后选择Build Phases选项卡 并展开名为 Run Build Tool Plug-ins的部分 然后点击加号图标 选择GRPCProtobufGenerator并点击Add

    该插件扫描目标目录中的proto文件 并可通过JSON配置文件进行配置 现在我将它们添加到目标中

    JSON文件配置要生成的代码 由于这是App 我只需要消息和客户端 不需要服务器代码 现在可以重新编译App以生成代码 作为安全措施 第一次使用时 系统会要求你信任该插件

    一切设置完毕 现在可以调用服务了 我将打开RaceScheduleView

    并导入所需模块

    核心模块提供 常见的gRPC运行时组件 HTTP模块提供网络代码 SwiftProtobuf让我们能够 与Protobuf消息交互 接下来 我将向视图 添加task修饰符 在其中发出请求 在task内部 我将使用 withGRPCClient函数创建客户端 我将在do catch块中执行此操作 现在只打印错误 gRPC Swift让你 配置用于网络的实现 我将使用基于SwiftNIO的实现 连接到Mac上本地运行的服务器

    传递给闭包的客户端只了解服务器 它对服务一无所知 这就是生成的代码发挥作用的地方 我将创建一个SwiftKart客户端

    并用gRPC客户端初始化它

    然后我将创建请求

    调用list races RPC 并等待响应

    最后 我将用新数据更新视图 通过将服务器响应映射 到视图使用的数据模型

    就这样 我们已从 本地服务器获取了比赛日程 Finite Loops听起来很有趣 即将开始 在继续之前 我需要做一个重要的更改

    目前 每次视图出现时 App都会创建新的gRPC客户端 这意味着每个视图 都需要建立与服务器的连接 增加了不必要的延迟 相反 App应该创建客户端 并在视图间共享 以便连接可以被复用

    我可以通过App环境传播客户端 客户端也应该断开连接 当App进入后台时 以释放资源 在Xcode中 我将添加 之前编写的客户端管理器代码

    然后打开App入口点 并创建管理器实例

    我将通过environment修饰符 使其对子视图可用 当场景进入后台阶段时 也应该断开客户端连接 为此 我将创建一个 scene phase属性

    然后监视其变化

    我的管理器类延迟连接 被要求提供客户端时 因此不需要做任何事情 当场景进入活跃状态时 现在我将在RaceScheduleView 中使用该管理器 我将把它添加到视图中

    并使其在预览中可用

    最后用对管理器的调用 替换withGRPCClient调用

    这就完成了App设置 通过代码与服务通信 由Protobuf中定义的 服务API生成 除服务API外 Protobuf还提供消息交换格式 SwiftProtobuf有一个代码生成器 让你能直接使用Swift类型 来表示你的消息 例如 我可以创建一条Race消息 并用相关信息填充字段 当gRPC在客户端和 服务器之间发送消息时 它将消息序列化为二进制表示 它使用唯一字段编号 而非名称来标识每个字段 因此 Protobuf消息 大约是等效JSON消息大小的一半 减少消息大小对移动App很有益 其中最小化数据传输有助于提升 网络调用的性能 这在网络条件差时尤为重要 这种效率对其他环境也很有益 例如服务间通信 以及进程间通信 例如Apple开源的 Containerization框架 它使用gRPC Swift 通过虚拟套接字进行通信 在宿主操作系统与 Linux虚拟机之间 gRPC Swift也是 云服务中的关键组件 例如Private Cloud Compute iCloud钥匙串和照片 以及SharePlay文件共享 但我们的应用不仅限于外部服务 gRPC深入我们的内部基础设施 例如操作系统构建和发布系统 gRPC的突出特性之一是 对流式传输的一流支持 许多RPC 例如list races 只需向服务器发送 单条请求消息 服务器以单条响应消息回复 这称为一元RPC 但RPC可以流式传输 请求和响应消息 这意味着还有三种 其他类型的RPC可探索 客户端流式RPC是指 客户端向服务器发送任意数量的消息 服务器以单条响应消息回复 想象每辆卡丁车将 遥测数据流式传输到服务器 在服务器流式RPC中 客户端发送单条请求消息 服务器以任意数量的响应消息回复 想想实时更新 例如实时文字解说 最后一种类型是双向流式传输 客户端和服务器 可以相互发送任意数量的消息 我想到了一个好办法 可以在App中使用它 来提供实时比赛更新 请求消息将告知服务器 客户端订阅了哪些类型的事件 响应消息将包含相关事件 当发生变化时 客户端可向服务器发送更多消息 关于他们感兴趣接收哪些事件

    我的App一直向 Mac上运行的服务器发出请求 该服务器也是用Swift编写的 我们来看看 设置服务器非常简单 我创建一个服务器对象 用传输进行初始化 以及它应该提供的服务 要启动服务器 只需调用serve 服务只是一个类型 它实现由构建插件生成的协议 你可以看到我之前实现的 list races RPC 它是一个async函数 接收请求并返回响应 实现它只需查询数据库中的比赛 填充消息然后返回它 为了集成流式RPC 我将更新服务定义 然后切换到服务器并重新生成代码 以便实现新的RPC 完成后 我将更新App来调用它 首先向服务定义中添加 FollowRace RPC 由于RPC流式传输请求和响应消息 我需要在输入和输出前 添加stream关键字 然后需要定义消息 请求消息包含要关注的比赛名称 以及要订阅的事件类型列表 以enum表示 响应类型有一个oneof字段 就像带有关联值的Swift enum 消息可以保存每辆卡丁车的位置 或当前比赛名次 它们被定义为单独的消息 服务定义更新完毕后 我将切换到Xcode中的服务器 以便实现新的RPC 我将构建项目以重新生成代码

    现在出现了构建错误 因为协议有新的要求 我尚未实现 因此我将填写一个桩代码

    这与list races RPC有所不同 因为有流式传输 请求参数是请求消息的async序列 响应参数是一个对象 用于向客户端写入响应消息 我知道需要同时处理两个数据流 因此我需要一个task group

    我需要等待第一条请求消息 以便了解要追踪的比赛名称 以及调用方感兴趣的事件 我将创建一个async迭代器 并等待第一条消息 我将把事件存储在 受mutex保护的集合中 因为两个不同的任务需要并发访问它 然后我将向task group添加一个任务 它调用实时比赛追踪器 传入要关注的比赛名称

    这给了我一个事件的async序列 然后我可以过滤 仅包含客户端当前感兴趣的事件

    我将迭代已过滤的事件

    并创建一条空响应消息

    然后 对事件进行switch并填充消息 首先 我将映射 追踪器中卡丁车位置的数组 到RPC使用的数据类型 然后对名次执行同样的操作

    现在我将消息写入客户端

    还有几件事需要完成 首先是继续处理请求消息 因为调用方可能会更改感兴趣的事件 我们将使用请求流的结束 作为客户端不再需要更多事件的信号 以便取消task group中运行的任务 停止发送回消息 最后我将重启服务器 以便客户端能调用新的RPC

    服务中已实现该RPC 现在我可以更新App了 我将打开App的Xcode项目 并更新proto文件 以包含新的RPC和消息

    我将构建项目以重新生成gRPC代码

    然后导航到RaceInfoView 添加指向早先创建的 LiveStreamView的NavigationLink 然后打开直播视图

    它显示一张地图 将绘制标注 表示比赛中每辆卡丁车的位置 还有一个工具栏按钮 可打开弹出页面 以显示实时排行榜 showLeaderboard属性追踪是否显示 视图已经具有属性 用于存储我感兴趣的各种状态 我只需要调用RPC 并连接从服务器接收到的数据 首先 我将添加之前使用的导入 然后通过environment注入客户端

    像之前一样 我将创建一个task

    并调用manager.withClient

    然后我将创建一个kart客户端

    并调用FollowRace RPC

    它的结构与一元list races RPC不同 它有两个闭包 一个用于写入请求消息 另一个用于处理响应消息 每次showLeaderboard的值变化时 需要发送一条请求消息 我将使用AsyncStream随时间追踪它 并将其续接作为属性存储

    当showLeaderboard发生变化时 我将向续接提供新值

    我将在task中创建 AsyncStream及其续接

    我需要向流提供 showLeaderboard的当前值 作为初始值 在RPC的第一个闭包中 我可以迭代流

    并为每个值向服务器发送消息

    如果正在显示排行榜 我将添加名次事件

    然后将消息写入服务器

    在响应闭包中 我将迭代消息并为每个事件 更新视图状态 我将使用辅助方法来处理事件

    我将对事件进行switch 并将每个映射到视图使用的数据类型

    我将从卡丁车位置开始 然后对名次执行同样的操作

    最后 我将迭代响应消息 并为每个事件调用辅助方法

    我们来看看效果

    一场比赛即将在Apple Park开始 他们正向彩虹拱门方向行驶 现在看来他们正在向右转向鸭子池塘 Monty领先 Pepper和Bo紧随其后 很好 但我还没有达到我的目标 将信息提供给观众 因为服务在本地运行 我们将其部署到云端 以便所有使用App的人都可以访问 我使用Google Cloud Platform 托管我的服务 但你也可以使用其他平台 例如AWS或Fly.io 方法类似 但具体步骤会有所不同 大多数服务器运行Linux 这也是我今天部署的目标 我不需要修改任何代码 但我需要打包服务器可执行文件 连同其运行时依赖项打包成容器镜像 然后将镜像发布到 云提供商的镜像仓库 之后我将创建一个部署 最后更新App以指向已部署的服务 首先创建一个Containerfile 它描述了构建容器镜像所需的步骤 我将使用swift:latest 作为基础镜像 接下来 我将设置工作目录 并复制包清单和源文件 然后以Release模式构建服务器 并将其复制到已知位置 此时 镜像中 已有服务器可执行文件 但它还包含整个Swift工具链 我不需要所有这些来运行我的服务器 这会使镜像比所需的大得多

    我将使用多阶段构建 并将二进制文件复制到 swift:slim运行时镜像中 最后 暴露端口 并将入口点设置为服务器

    Containerfile就这样写完了 此时我会构建并发布镜像 到容器仓库 但这需要几分钟 因此我将使用之前发布的镜像 在终端中 我可以使用 gcloud run deploy命令

    我将提供部署名称 镜像名称和地区 然后需要指定我的服务使用http2 并允许未经身份验证的请求

    部署完成后 它会打印出服务的URL 更新客户端时会需要它 所以现在先复制

    我将切换回App并打开 ClientManager 我将更新连接目标为服务的DNS名称 来自部署命令 然后通过更改传输安全选项启用TLS 从明文更改为TLS

    我们来测试一下

    看来我们赶上了 Finite Loops比赛的开始

    Pepper的起步真是糟糕 Monty占据第一位 Mycroft和Kiko紧随其后

    车手们正转向Infinite Loop园区

    Pepper夺回了几个名次

    看来这将是一场精彩的比赛

    我向你展示了如何使用gRPC Swift 在App中构建出色的实时体验 以及它如何简化App到服务器的通信 从定义服务 生成代码 一直到实现并将服务部署到云端 而这只是开始 gRPC Swift内置了大量功能 帮助你将应用从原型推向生产环境 无论是与其他Swift包的集成 例如Swift OTel 或Swift service lifecycle 还是高级连接管理功能 如自定义传输和名称解析器 以及客户端负载均衡 你现在已准备好 在App中使用gRPC了 何不尝试构建 App与服务器交互的原型 看看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.

Developer Footer

  • 视频
  • WWDC26
  • 使用 gRPC 和 Swift 构建实时 App 及服务
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • Apple 智能
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习与 AI
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • Mini Apps Partner Program
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    阅读最近新闻。
    获取 Apple Developer App。
    版权所有 © 2026 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则