Getting started
Connect-Swift is a small library (<200KB!) that provides support for using generated, type-safe, and idiomatic Swift APIs to communicate with your app's servers using Protobuf.
Imagine a world where you can jump right into building products
and focus on the user experience without needing to handwrite REST/JSON
endpoints or models that conform to Codable
— instead using generated APIs
that utilize the latest Swift features and are guaranteed to match the server's
modeling. Furthermore, imagine never having to worry about serialization again,
and being able to easily write tests with generated mocks that conform to the
same protocol as the real implementations.
All of this is possible with Connect-Swift.
In this guide, we'll use Connect-Swift to create a chat app for ELIZA, a very simple natural language processor built in the 1960s to represent a psychotherapist. The ELIZA service is implemented using Connect-Go, is already up and running in production, and supports both the gRPC-Web and Connect protocols - both of which can be used with Connect-Swift for this tutorial. The APIs we'll be using are defined in a Protobuf schema that we'll use to generate a Connect-Swift client.
This tutorial should take ~10 minutes from start to finish.
Define a Protobuf service
We'll start by creating a Protobuf schema that defines the ELIZA
API. In your shell, create a .proto
file:
touch eliza.proto
Open the newly created eliza.proto
file in your editor and add:
syntax = "proto3";
package buf.connect.demo.eliza.v1;
message SayRequest {
string sentence = 1;
}
message SayResponse {
string sentence = 1;
}
service ElizaService {
rpc Say(SayRequest) returns (SayResponse) {}
}
This file declares a buf.connect.demo.eliza.v1
Protobuf package,
a service called ElizaService
, and a single unary
(single-request / single-response) method
called Say
. Under the hood, these components will be used to form the path
of the API's HTTP URL.
The file also contains two models, SayRequest
and SayResponse
, which
are the input and output for the Say
RPC method.
Generate code
We're going to generate our code using Buf, a modern replacement for Google's protobuf compiler. Specifically, we will use remote plugins, a feature of the Buf Schema Registry. This requires installing Buf's CLI:
brew install bufbuild/buf/buf
To configure Buf, scaffold a basic buf.yaml
by running:
buf mod init
Next, tell Buf how to generate code by creating a new
buf.gen.yaml
file:
touch buf.gen.yaml
...and adding this content to it:
version: v1
plugins:
- plugin: buf.build/bufbuild/connect-swift
opt: >
GenerateAsyncMethods=true,
GenerateCallbackMethods=true,
Visibility=Public
out: Generated
- plugin: buf.build/apple/swift
opt: Visibility=Public
out: Generated
With those configuration files in place, we can now generate code:
buf generate
In your Generated
directory, you should now see some generated Swift files:
Generated
├── eliza.connect.swift
└── eliza.pb.swift
The .connect.swift
file contains both a Swift protocol interface for the
ElizaService
, as well as a production client that conforms to this interface.
The .pb.swift
file was generated by Apple's
SwiftProtobuf plugin and contains the corresponding Swift
models for the SayRequest
and SayResponse
we defined in our Protobuf file.
Add the Connect Swift package
We're ready to create the app that will consume these generated APIs. Open
Xcode and create a new SwiftUI project called Eliza
.
Next, add a dependency on the Connect-Swift
package in Xcode by clicking
File
> Add Packages...
:
In the popup window, click into the Search or Enter Package URL
text field
in the top right and paste the Connect-Swift GitHub URL:
https://github.com/bufbuild/connect-swift
Ensure the Connect
library is selected, then
click Add Package
to confirm the package addition. Note that this will
automatically add the required SwiftProtobuf
package as well:
Alternative: Use CocoaPods
CocoaPods is also supported as an alternative to Swift Package Manager.
To use Connect-Swift with CocoaPods, simply add this line to your Podfile
:
pod 'Connect-Swift'
pod 'SwiftProtobuf'
Integrate into the app
First, add the generated .swift
files from the previous steps to your
project by dragging the Generated
directory into Xcode.
At this point, your app should build successfully.
To create the chat view, replace the contents of ContentView.swift
with:
Click to expand ContentView.swift
import Combine
import SwiftUI
struct Message: Identifiable {
enum Author {
case eliza
case user
}
typealias ID = UUID // Required for `Identifiable`
let id = UUID()
let message: String
let author: Author
}
final class MessagingViewModel: ObservableObject {
private let elizaClient: Buf_Connect_Demo_Eliza_V1_ElizaServiceClientInterface
@MainActor @Published private(set) var messages = [Message]()
init(elizaClient: Buf_Connect_Demo_Eliza_V1_ElizaServiceClientInterface) {
self.elizaClient = elizaClient
}
func send(_ sentence: String) async {
let request = Buf_Connect_Demo_Eliza_V1_SayRequest.with { $0.sentence = sentence }
await self.addMessage(Message(message: sentence, author: .user))
let response = await self.elizaClient.say(request: request, headers: [:])
await self.addMessage(Message(
message: response.message?.sentence ?? "No response", author: .eliza
))
}
@MainActor
private func addMessage(_ message: Message) {
self.messages.append(message)
}
}
struct ContentView: View {
@State private var currentMessage = ""
@ObservedObject private var viewModel: MessagingViewModel
init(viewModel: MessagingViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
ScrollViewReader { listView in
// ScrollViewReader crashes in iOS 16 with ListView:
// https://developer.apple.com/forums/thread/712510
// Using ScrollView + ForEach as a workaround.
ScrollView {
ForEach(self.viewModel.messages) { message in
VStack {
switch message.author {
case .user:
HStack {
Spacer()
Text("You")
.foregroundColor(.gray)
.fontWeight(.semibold)
}
HStack {
Spacer()
Text(message.message)
.multilineTextAlignment(.trailing)
}
case .eliza:
HStack {
Text("Eliza")
.foregroundColor(.blue)
.fontWeight(.semibold)
Spacer()
}
HStack {
Text(message.message)
.multilineTextAlignment(.leading)
Spacer()
}
}
}
.id(message.id)
}
}
.onChange(of: self.viewModel.messages.count) { messageCount in
listView.scrollTo(self.viewModel.messages[messageCount - 1].id)
}
}
HStack {
TextField("Write your message...", text: self.$currentMessage)
.onSubmit { self.sendMessage() }
.submitLabel(.send)
Button("Send", action: { self.sendMessage() })
.foregroundColor(.blue)
}
}
.padding()
}
private func sendMessage() {
let messageToSend = self.currentMessage
if messageToSend.isEmpty {
return
}
Task { await self.viewModel.send(messageToSend) }
self.currentMessage = ""
}
}
Lastly, replace the contents of ElizaApp.swift
with:
Click to expand ElizaApp.swift
import Connect
import SwiftUI
@main
struct ElizaApp: App {
@State private var client = ProtocolClient(
httpClient: URLSessionHTTPClient(),
config: ProtocolClientConfig(
host: "https://demo.connect.build",
networkProtocol: .connect, // Or .grpcWeb
codec: ProtoCodec() // Or JSONCodec()
)
)
var body: some Scene {
WindowGroup {
ContentView(viewModel: MessagingViewModel(
elizaClient: Buf_Connect_Demo_Eliza_V1_ElizaServiceClient(client: self.client)
))
}
}
}
Build and run the app, and you should be able to chat with Eliza! 🎉

Breaking it down
Let's dive into what some of the code above is doing, particularly regarding how it is interacting with the Connect library.
Creating a ProtocolClient
First, the ElizaApp
creates and stores an instance of ProtocolClient
.
This type is configured with various options specifying which HTTP client should
be used (the default being URLSession
), how data should be encoded/decoded
(i.e., JSON or Protobuf binary), and which protocol to use (in this case,
the Connect protocol).
If we wanted to use JSON instead of Protobuf and to enable request gzipping, we'd only need to make a simple 2 line change:
private var client = ProtocolClient(
httpClient: URLSessionHTTPClient(),
config: ProtocolClientConfig(
host: "https://demo.connect.build",
networkProtocol: .connect,
codec: JSONCodec(),
requestCompression: .init(minBytes: 50_000, pool: GzipCompressionPool())
)
)
HTTP client's behavior can be customized by subclassing
the URLSessionHTTPClient
or by creating a new type that conforms to the HTTPClientInterface
protocol and passing it as the httpClient
. For more customization options,
see the documentation on using clients.
Using the generated code
Take a look at the MessagingViewModel
class above. It is initialized with an
instance of a type that conforms to
Buf_Connect_Demo_Eliza_V1_ElizaServiceClientInterface
- the Swift protocol
that was generated from the ElizaService
Protobuf service definition.
Accepting a protocol, rather than the
generated Buf_Connect_Demo_Eliza_V1_ElizaServiceClient
concrete type that conforms to the protocol, allows for injecting mock classes
into the view model for testing. We won't get into mocks and testing here, but
you can check out the testing docs for details and examples.
Whenever the send(...)
function is invoked by the SwiftUI view, the
view model creates a Buf_Connect_Demo_Eliza_V1_SayRequest
and
passes it to the say(...)
function on the generated client before awaiting a response from the server.
All of this is done using type-safe generated APIs from the Protobuf
file we wrote earlier.
Although this example uses Swift's async/await APIs, traditional
closures/callbacks can also be generated by Connect-Swift, and opening up the
generated .connect.swift
file will reveal both interfaces. This behavior
can be customized using generator options.
More examples
There are more detailed examples that you can explore within the Connect-Swift repository on GitHub. These examples demonstrate:
- Using streaming APIs
- Integrating with Swift Package Manager
- Integrating with CocoaPods
- Using the Connect protocol
- Using the gRPC-Web protocol
Using the gRPC-Web protocol instead of the Connect protocol
Connect-Swift supports both the Connect protocol and the gRPC-Web protocol. Instructions for switching between the two can be found here.
We recommend using Connect-Swift over gRPC-Swift even if you're using the gRPC-Web protocol for a few reasons:
- Idiomatic, typed APIs. No more hand-writing REST/JSON endpoints and
Codable
conformances. Connect-Swift generates idiomatic APIs that utilize the latest Swift features such as async/await and eliminates the need to worry about serialization. - First-class testing support. Connect-Swift generates both production and mock implementations that conform to the same protocol interfaces, enabling easy testability with minimal handwritten boilerplate.
- Easy-to-use tooling. Connect-Swift integrates with the Buf CLI, enabling remote code generation without having to install and configure local dependencies.
- Flexibility. Connect-Swift uses
URLSession
. The library provides the option to swap this out, as well as the ability to register custom compression algorithms and interceptors. - Binary size. The Connect-Swift library is very small (<200KB) and does not require any third party networking dependencies.
If your back-end services are already using gRPC today, Envoy provides support for converting between gRPC and gRPC-Web, enabling you to use gRPC-Web through Connect-Swift without having to change any existing gRPC APIs.