Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2026 Developer ToolsSwift

WWDC26 · 24 min · Developer Tools / Swift

Build real-time apps and services with gRPC and Swift

Build engaging live experiences with gRPC in your Swift app and backend. gRPC is an open-source RPC framework designed for high-performance, bidirectional streaming APIs. Explore how the gRPC Swift package provides a modern, safe runtime built with Swift concurrency. Learn how integrated tools streamline your workflow and help you deliver real-time features with ease.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

  • 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

Code shown on screen · 23 snippets

ListRaces RPC definition bash · at 3:38 ↗
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;
}
grpc-swift-proto-generator-config.json json · at 5:55 ↗
{
    "generate": {
        "clients": true,
        "servers": false,
        "messages": true
    }
}
Add gRPC imports swift · at 6:24 ↗
import GRPCCore
import GRPCNIOTransportHTTP2
import SwiftProtobuf
Create a gRPC client connected to a local server swift · at 6:38 ↗
.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)")
    }
}
Call the ListRaces RPC and update the view swift · at 7:14 ↗
.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)")
    }
}
ClientManager.swift swift · at 8:30 ↗
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
        }
    }
}
Propagate ClientManager to child views swift · at 8:39 ↗
import SwiftUI

@main
struct SwiftKartApp: App {
    let manager = ClientManager()

    var body: some Scene {
        WindowGroup {
            RaceScheduleView()
                .environment(manager)
        }
    }
}
Disconnect ClientManager when the scene enters the background phase swift · at 8:52 ↗
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
            }
        }
    }
}
Inject ClientManager into the view via @Environment swift · at 9:12 ↗
@Environment(ClientManager.self) var manager
Replace withGRPCClient with manager.withClient swift · at 9:21 ↗
.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)")
    }
}
Using SwiftProtobuf swift · at 9:41 ↗
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()
Server swift · at 12:32 ↗
let server = GRPCServer(
    transport: .http2NIOPosix(
        address: .ipv4(host: "127.0.0.1", port: 8080),
        transportSecurity: .plaintext
    ),
    services: [Service()]
)
try await server.serve()
Service swift · at 12:45 ↗
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
    }
}
swift_kart_service.proto bash · at 13:20 ↗
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;
}
FollowRace stub swift · at 14:16 ↗
func followRace(
    request: RPCAsyncSequence<FollowRaceRequest, any Error>,
    response: RPCWriter<FollowRaceResponse>,
    context: ServerContext
) async throws {
    throw RPCError(code: .unimplemented, message: "FollowRace is unimplemented")
}
Implement the FollowRace RPC swift · at 14:38 ↗
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()
    }
}
swift_kart_service.proto bash · at 16:39 ↗
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;
}
swift_kart_service.proto bash · at 16:40 ↗
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;
}
Navigation link to LiveStreamView swift · at 16:56 ↗
NavigationLink(destination: LiveStreamView(race: race)) {
    Text("Live stream")
}
Call the FollowRace RPC in the LiveStreamView swift · at 17:32 ↗
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())
    }
}
Containerfile bash · at 20:55 ↗
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"]
Deploy service bash · at 21:56 ↗
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
Target deployed service swift · at 22:22 ↗
static func makeTransport() throws -> HTTP2ClientTransport.TransportServices {
    try .http2NIOTS(
        target: .dns(host: "wwdc-demo-server-863666503339.us-central1.run.app"),
        transportSecurity: .tls
    )
}

Resources