WWDC Quick Look 💓 By SwiftGGTeam
Build real-time apps and services with gRPC and Swift

Build real-time apps and services with gRPC and Swift

观看原视频

Highlight

Apple 为 Swift 提供了原生 gRPC 运行时,开发者通过 Xcode Build Plugin 从 .proto 文件自动生成强类型的客户端和服务端代码,用 async/awaitAsyncSequence 实现一元调用与双向流式通信,并可将 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 实现,支持 TLS
  • SwiftKartService.Client(wrapping: client) 把通用 gRPC 客户端包装成类型安全的业务客户端
  • listRacesasync 方法,直接用 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 struct
  • serializedBytes() 生成紧凑的二进制数据
  • 字段编号替代字段名,减少传输体积
  • 对移动网络环境尤其有利

双向流式 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 的带关联值枚举,消息只能是 locationsstandings 之一
  • 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.messagesAsyncSequence,用 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:32GRPCProtobufGenerator 是 Xcode 的 Build Tool Plugin。它在编译前扫描 Target 目录下的 .proto 文件,根据 JSON 配置生成对应的 Swift 代码。首次使用时会弹出安全提示要求信任插件。

配置文件的三个开关:

  • clients: true — 生成客户端调用代码
  • servers: true — 生成服务端协议桩代码
  • messages: true — 生成 Protobuf 消息模型

iOS App 通常只需要 clientsmessages,服务端则需要全部三个。

四种 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

评论

GitHub Issues · utterances