Skip to main content

Using clients

Configuration

Supported protocols

Connect-Swift currently supports 3 protocols:

  • The new Connect protocol, a simple, HTTP-based protocol that works over HTTP/1.1 or HTTP/2. It takes the best parts of gRPC/gRPC-Web, including streaming, and packages them into a protocol that works well on all platforms, including mobile. By default, JSON- and binary-encoded Protobuf is supported.
  • The gRPC protocol: Allows clients to communicate with existing gRPC services.
  • The gRPC-Web protocol: Allows clients to communicate with existing gRPC-Web services. The main difference between gRPC and gRPC-Web is that gRPC-Web does not utilize HTTP trailers in the protocol.

If your backend services are already using gRPC today, Envoy provides support for converting requests made using the Connect and gRPC-Web protocols to gRPC, enabling you to use Connect-Swift without the SwiftNIO dependency.

Switching between the Connect and gRPC-Web protocols is a simple 1-line change when configuring the ProtocolClient:

import Connect

let protocolClient = ProtocolClient(
httpClient: URLSessionHTTPClient(),
config: ProtocolClientConfig(
host: "https://demo.connectrpc.com",
networkProtocol: .connect, // Or .grpcWeb
codec: ProtoCodec()
)
)

Using gRPC requires including ConnectNIO and swapping out the HTTP client, but it is equally straightforward:

import Connect
import ConnectNIO

let host = "https://demo.connectrpc.com"
let protocolClient = ProtocolClient(
httpClient: NIOHTTPClient(host: host),
config: ProtocolClientConfig(
host: host,
networkProtocol: .grpc,
codec: ProtoCodec()
)
)

Note that these options are mutually exclusive. If you'd like to use different protocols with different APIs, create one ProtocolClient for each protocol.

Client config

Examples of built-in config options include gzip compression for requests and responses, support for interceptors, and the ability to switch between JSON and Protobuf binary encodings. The full list of config options can be found in the Connect-Swift repository.

Compression

Request compression and response decompression are provided through compression pools that contain logic for compressing and decompressing data. Gzip support is available by default, and support for additional compression algorithms can be provided by replicating the functionality provided by the GzipCompressionPool and passing it to the ProtocolClientConfig.

HTTP stack

By default, HTTP networking is done using URLSession via the URLSessionHTTPClient wrapper. Its init() accepts a URLSessionConfiguration to configure the underlying session. Furthermore, URLSessionHTTPClient can also be subclassed if desired.

The ConnectNIO library in the Connect-Swift package also includes an NIOHTTPClient which is backed by SwiftNIO, enabling it to handle trailers (which are not supported by URLSession).

If you wish to write a custom HTTP client, this can be achieved by conforming to the HTTPClientInterface protocol and passing it as the httpClient when initializing a ProtocolClient.

Using generated clients

async/await

Generated clients support Swift's latest async/await APIs, making it easy to use code generated by Connect in async contexts.

Async interfaces are provided by default, and are configurable using the GenerateAsyncMethods option:

import Connect

// ProtocolClient is usually stored and passed to generated clients.
let protocolClient = ProtocolClient(
httpClient: URLSessionHTTPClient(),
config: ProtocolClientConfig(
host: "https://demo.connectrpc.com", // Base URL for APIs
networkProtocol: .connect, // Or .grpcWeb
codec: ProtoCodec() // Or JSONCodec()
)
)
let elizaClient = Eliza_V1_ElizaServiceClient(client: protocolClient)

...

// Performed within an async context.
let request = Eliza_V1_SayRequest.with { $0.sentence = "hello world" }
let response = await elizaClient.say(request: request, headers: [:])
print(response.message?.sentence)

For server-streaming RPCs, the corresponding method on the client returns an *AsyncStream object which allows the caller to send data over the stream and to iterate over updates from the server using the for await...in statement:

let stream = elizaClient.converse(headers: [:])
Task {
for await result in stream.results() {
switch result {
case .headers(let headers):
...
case .message(let message):
...
case .complete(let code, let error, let trailers):
...
}
}
}

let request = ConverseRequest.with { $0.sentence = "hello world" }
try stream.send(request)

Callbacks

If you prefer a callback-based approach, callbacks/closures can be used instead.

These interfaces are not generated by default, but are configurable using the GenerateCallbackMethods option:

import Connect

// ProtocolClient is usually stored and passed to generated clients.
let protocolClient = ProtocolClient(
httpClient: URLSessionHTTPClient(),
config: ProtocolClientConfig(
host: "https://demo.connectrpc.com", // Base URL for APIs
networkProtocol: .connect, // Or .grpcWeb
codec: ProtoCodec() // Or JSONCodec()
)
)
let elizaClient = Eliza_V1_ElizaServiceClient(client: protocolClient)

...

let request = Eliza_V1_SayRequest.with { $0.sentence = "hello world" }
let cancelable = elizaClient.say(request: request, headers: [:]) { response in
print(response.message?.sentence)
}

// cancelable.cancel()

For server-streaming RPCs, the corresponding method on the client accepts an onResult closure which is called each time an update is received from the server, and returns a *Stream object which allows the caller to send data over the stream:

let stream = elizaClient.converse(onResult: { result in
switch result {
case .headers(let headers):
...
case .message(let message):
...
case .complete(let code, let error, let trailers):
...
}
})

let request = ConverseRequest.with { $0.sentence = "hello world" }
try stream.send(request)
...
stream.close()