Highlight
Apple 为 Swift 提供了原生 gRPC 运行时,开发者通过 Xcode Build Plugin 从
.proto文件自动生成强类型的客户端和服务端代码,用async/await和AsyncSequence实现一元调用与双向流式通信,并可将 Swift 服务端打包为容器镜像部署到云端。
核心内容
手写网络代码的困境
做 iOS 开发时,调用后端接口通常要走这几步:读文档、手写 URLSession 代码、定义 Codable 模型、处理序列化错误。文档可能过时,模型字段可能拼错,联调时才发现问题。如果要做实时双向通信,还得引入 WebSocket,自己处理心跳、重连、状态机。这套流程费时且容易出错。
gRPC 的解决思路是把 API 定义从实现中抽离出来。你用 Protocol Buffers(Protobuf)写一份 .proto 文件描述服务接口,代码生成器自动产出 Swift 类型和调用代码。这份 .proto 文件就是唯一的真源,iOS 端和服务端共用同一份定义,类型安全从编译期就得到保障。
从 .proto 到 Xcode:工程体验全面升级
Session 以一个卡丁车联赛 App 为例,演示了完整的端到端流程。
(03:38)首先定义服务接口。在 .proto 文件中声明一个 SwiftKartService,包含 ListRaces RPC:
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;
}
(05:55)然后在 Xcode 中添加两个 Swift Package:grpc-swift-nio-transport 提供基于 SwiftNIO 的网络传输层,grpc-swift-protobuf 提供 Build Plugin 用于代码生成。在 Target 的 Build Phases 里添加 GRPCProtobufGenerator 插件,再配一个 JSON 文件告诉插件只生成客户端代码:
{
"generate": {
"clients": true,
"servers": false,
"messages": true
}
}
编译时插件自动扫描 .proto 文件并生成 Swift 代码,不需要手动跑 protoc 命令。
客户端调用:像调用本地函数一样调用远程服务
(06:24)在 SwiftUI View 中导入三个模块:
import GRPCCore
import GRPCNIOTransportHTTP2
import SwiftProtobuf
(06:38)用 withGRPCClient 创建客户端并发起调用:
.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)")
}
}
关键点:
withGRPCClient自动管理连接生命周期,闭包结束时关闭连接.http2NIOTS使用 SwiftNIO Transport Services 实现,支持 TLSSwiftKartService.Client(wrapping: client)把通用 gRPC 客户端包装成类型安全的业务客户端listRaces是async方法,直接用await获取响应race.startTime.date把 Protobuf 的Timestamp转成 Swift 的Date
连接复用与生命周期管理
(07:55)每次 View 出现时都新建连接会增加延迟。正确的做法是在 App 级别维护一个共享的客户端连接池。
(08:30)Session 提供了一个 ClientManager 实现:
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
}
}
}
关键点:
Mutex(来自Synchronization框架)保护连接状态,满足 Swift 6 严格并发检查connectIfNecessary()懒加载连接,已连接时直接复用runConnections()在后台 Task 中持续维护 HTTP/2 连接beginGracefulShutdown()优雅关闭连接,避免强制断开造成数据丢失
(08:39)在 App 入口注入 ClientManager:
@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
}
}
}
}
关键点:
.environment(manager)把连接管理器注入 SwiftUI 环境- 监听
scenePhase变化,App 进后台时主动断开连接,释放资源
(09:12)子 View 中通过 @Environment 获取管理器:
@Environment(ClientManager.self) var manager
(09:21)调用方式改为 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 { ... }
}
} catch {
print("gRPC error: \(error)")
}
}
Protobuf 序列化:比 JSON 更紧凑
(09:41)Protobuf 消息序列化时使用字段编号而非字段名标识数据,同等内容的二进制体积约为 JSON 的一半:
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()
关键点:
SwiftProtobuf把 Protobuf 消息映射为原生 Swift structserializedBytes()生成紧凑的二进制数据- 字段编号替代字段名,减少传输体积
- 对移动网络环境尤其有利
双向流式 RPC:实时数据推送
(11:06)gRPC 支持四种 RPC 类型:一元(unary)、客户端流、服务端流、双向流。Session 重点演示了双向流在实时场景中的应用。
(13:20)在 .proto 文件中定义双向流 RPC:
service SwiftKartService {
rpc ListRaces(ListRacesRequest) returns (ListRacesResponse);
rpc FollowRace(stream FollowRaceRequest) returns (stream FollowRaceResponse);
}
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;
}
}
关键点:
stream关键字标记双向流oneof对应 Swift 的带关联值枚举,消息只能是locations或standings之一RaceEventType枚举让客户端动态订阅感兴趣的事件类型
(12:45)服务端实现 ListRaces:
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
}
}
关键点:
- 实现
SimpleServiceProtocol协议,方法由 Build Plugin 自动生成 - 纯
async函数,用await查询数据库 ServerContext提供调用元数据,如超时、认证信息
(14:38)服务端实现双向流 FollowRace:
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()
}
}
关键点:
RPCAsyncSequence是客户端发来的请求流RPCWriter用于向客户端写入响应消息withThrowingTaskGroup并发处理请求消费和事件推送Mutex保护eventTypes集合,支持客户端动态调整订阅内容- 客户端关闭流时(
iterator.next()返回nil),取消所有任务
(17:32)客户端调用双向流:
.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
)
}
}
}
关键点:
followRace有两个闭包:第一个写请求,第二个处理响应AsyncStream把 UI 状态变化(showLeaderboard)桥接到请求流- 用户打开排行榜时动态追加
.standings事件订阅 responseStream.messages是AsyncSequence,用for try await消费@MainActor保证 UI 更新在主线程执行
部署到云端
(20:55)服务端用多阶段构建打包为容器镜像:
FROM swift:latest AS builder
WORKDIR /app
COPY Package.swift Package.resolved .
COPY Sources/ Sources/
RUN swift build -c release --product server
RUN cp "$(swift build -c release --show-bin-path)/server" /usr/bin/server
FROM swift:slim
COPY --from=builder /usr/bin/server /usr/bin/server
EXPOSE 8080
ENTRYPOINT ["/usr/bin/server"]
关键点:
- 多阶段构建:编译阶段用完整 Swift 工具链,运行时只保留二进制和 slim 镜像
- 显著减小最终镜像体积
swift build -c release生成优化后的 release 二进制
(21:56)部署到 Google Cloud Run:
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
关键点:
--use-http2必须开启,gRPC 基于 HTTP/2--allow-unauthenticated允许公开访问
(22:22)客户端更新为连接云端服务:
static func makeTransport() throws -> HTTP2ClientTransport.TransportServices {
try .http2NIOTS(
target: .dns(host: "wwdc-demo-server-863666503339.us-central1.run.app"),
transportSecurity: .tls
)
}
关键点:
.dns替代.ipv4,支持域名解析- 生产环境必须使用
.tls
详细内容
Xcode Build Plugin 的工作机制
(05:32)GRPCProtobufGenerator 是 Xcode 的 Build Tool Plugin。它在编译前扫描 Target 目录下的 .proto 文件,根据 JSON 配置生成对应的 Swift 代码。首次使用时会弹出安全提示要求信任插件。
配置文件的三个开关:
clients: true— 生成客户端调用代码servers: true— 生成服务端协议桩代码messages: true— 生成 Protobuf 消息模型
iOS App 通常只需要 clients 和 messages,服务端则需要全部三个。
四种 RPC 类型对比
| 类型 | 请求 | 响应 | 适用场景 |
|---|---|---|---|
| Unary | 单条 | 单条 | 普通 API 调用,如获取列表 |
| Client streaming | 多条 | 单条 | 批量上传,如卡丁车遥测数据 |
| Server streaming | 单条 | 多条 | 实时推送,如文字直播 |
| Bidirectional | 多条 | 多条 | 实时双向通信,如位置跟踪 + 排行榜 |
SwiftNIO 传输层
gRPC Swift 的网络层基于 SwiftNIO,提供两种传输实现:
http2NIOTS— 使用 Network.framework,推荐用于 Apple 平台http2NIOPosix— 使用 POSIX socket,推荐用于 Linux 服务端
(12:32)服务端使用 POSIX 实现:
let server = GRPCServer(
transport: .http2NIOPosix(
address: .ipv4(host: "127.0.0.1", port: 8080),
transportSecurity: .plaintext
),
services: [Service()]
)
try await server.serve()
Protobuf Edition 2024
Session 中的 .proto 文件使用了 edition = "2024",这是 Protobuf 的新版本语法。相比 proto3,Edition 2024 提供了更细粒度的字段行为控制,同时保持向后兼容。
核心启发
1. 用一套 .proto 文件统一前后端接口
做什么:把现有 App 的 REST API 迁移到 gRPC,前后端共用同一份 .proto 定义。
为什么值得做: eliminates 接口文档与实现不同步的问题,字段类型变更在编译期就能发现,联调时间大幅减少。
怎么开始:选一个数据交互频繁的模块(如用户资料、商品列表),写出 .proto 定义,用 Build Plugin 生成代码,逐步替换现有网络层。
2. 在 IM 或协作类 App 中用双向流替代 WebSocket
做什么:用 gRPC 双向流实现实时消息推送和状态同步。
为什么值得做:gRPC 提供强类型消息、自动重连、流控和负载均衡,比手写 WebSocket 状态机更可靠。AsyncSequence 让业务代码保持简洁。
怎么开始:定义一个双向流 RPC,客户端用 AsyncStream 桥接 UI 事件到请求流,服务端用 TaskGroup 并发处理多个客户端连接。
3. 把 Swift 服务端打包为容器部署到云端
做什么:用 Swift 写 gRPC 服务端,用多阶段 Docker 构建打包,部署到 Cloud Run、AWS ECS 或 Fly.io。
为什么值得做:Swift 服务端性能接近 C++,内存占用低于 Node.js 和 Python,加上 gRPC 的高效二进制序列化,整体资源成本更低。
怎么开始:参考 Session 中的 Containerfile,用 swift:slim 作为运行时镜像,确保部署时开启 HTTP/2。
4. 用 gRPC 做跨进程通信
做什么:在 macOS App 与辅助进程、或 host OS 与 Linux VM 之间用 gRPC 通信。
为什么值得做:Apple 的开源 Containerization 框架已经在内部使用 gRPC Swift 通过虚拟 socket 通信。这比 XPC 更灵活,且天然支持跨平台。
怎么开始:在 .proto 中定义进程间通信接口,用 .plaintext 传输安全模式连接本地 Unix domain socket。
5. 结合 Swift OTel 实现分布式追踪
做什么:在 gRPC 调用链中接入 OpenTelemetry,追踪请求从 iOS 端到服务端的完整路径。
为什么值得做:gRPC Swift 支持与 Swift OTel 集成,生产环境中定位延迟瓶颈和错误根因时,分布式追踪比日志更高效。
怎么开始:在服务端 ServerContext 中提取 trace context,在客户端调用时注入 span,用 Jaeger 或 Grafana Tempo 查看调用链。
关联 Session
- Session 262: Swift — Swift 6 的严格并发模型与
Sendable协议,是理解ClientManager中Mutex用法的基础 - Session 267: Swift Testing — 用 Swift Testing 为 gRPC 服务端写单元测试,验证 RPC 实现的正确性
- Session 268: Instruments responsiveness — 用 Instruments 分析 gRPC 连接的延迟和内存占用
- Session 389: Container machines — Apple 的容器化框架内部使用 gRPC Swift 做跨 VM 通信,与本 Session 的技术栈直接相关
评论
GitHub Issues · utterances