-
Desenvolva apps e serviços em tempo real com gRPC e Swift
Crie experiências ao vivo envolventes com gRPC no seu app e back-end em Swift. gRPC é um framework de RPC de código aberto projetado para APIs de streaming bidirecional de alto desempenho. Confira como o pacote gRPC Swift oferece um tempo de execução moderno e seguro, desenvolvido com a concorrência do Swift. Saiba como as ferramentas integradas simplificam seu fluxo de trabalho e ajudam você a disponibilizar recursos em tempo real com facilidade.
Capítulos
- 0:00 - Introdução
- 1:39 - Conheça o gRPC
- 2:13 - Visão geral do app e configuração de demonstração
- 3:30 - Definindo o RPC ListRaces
- 4:30 - Configurando o Xcode para gerar código gRPC
- 7:50 - Gerenciando o ciclo de vida do cliente gRPC
- 9:36 - Formato de Mensagem Protobuf e Eficiência Binária
- 12:33 - Implementando um RPC de Streaming Bidirecional
- 20:11 - Implantando o serviço
- 23:11 - Próximas etapas
Recursos
- About gRPC
- gRPC Swift Extras
- gRPC Swift Protobuf
- gRPC Swift NIO Transport
- gRPC Swift
- Swift on Server
Vídeos relacionados
WWDC25
WWDC24
WWDC23
-
Buscar neste vídeo...
-
-
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 ) }
-