Skip to content Documentation index for agents (llms.txt)
Glotto Beta
Get started

Swift

The Swift SDK is an idiomatic package emitted from the same GlottoIR as every other target. Operations under a declared resource hang off an accessor — client.pets.listPets() — while an operation you leave ungrouped stays a method on Client. Request/response shapes are Codable Swift types, so you work with real types rather than Data.

Quickstart

// In your Package.swift, add the generated package to `dependencies`
// (a local `.package(path:)` or your own published URL), then its library product:
.product(name: "Petstore", package: "Petstore")
import Foundation
import Petstore

let client = Client(
    baseURL: "https://api.petstore.example",
    token: ProcessInfo.processInfo.environment["PETSTORE_TOKEN"]!
)

// a single call
let pet = try await client.pets.createPet(body: NewPet(name: "Rex"))

// For an operation configured with pagination:
let pets = client.pets.listPetsScoped()
defer { pets.close() }
for try await pet in pets {
    print(pet.name)
}

Typed models

Each GlottoIR model becomes a Codable struct; operations decode and return the typed response and accept a typed request body. Discriminated unions resolve to the right enum case.

Typed errors

Operations throw an APIError struct carrying the parsed error body, with an APIErrorKind enum so callers switch error.kind / if case .notFound rather than matching status codes. See Errors.

Pagination

Scoped companions return a StreamScope that fetches another page only when you advance beyond the current page. Put close() in a defer block to release it when your code breaks, returns, or throws. Existing AsyncThrowingStream methods remain available; cancel their consuming task when leaving early. See Pagination.

Manual Page companions expose typed items and explicit nextPage() navigation. The throwing response() accessor decodes the full wrapper without another request. See manual pagination.

Retries & backoff

Pass RequestOptions to override headers, timeouts, retry counts and idempotency for an individual operation. Method defaults apply before caller overrides. See request options and defaults.

Transient failures (5xx, 429, transport errors) retry with exponential backoff and jitter, configurable per client. See Retries & timeouts.

SSE, NDJSON, and WebSocket streaming

SSE and NDJSON Scoped companions provide the same StreamScope ownership pattern as pagination. The opening deadline and retry policy apply until the first response byte. After that boundary, streams outlive the opening timeout and read failures surface without replay.

WebSocket operations retain their typed connection API. Put connection.close() in defer when leaving a message loop early. The upgrade uses the configured client, and a successful 101 ends the opening deadline before the first application frame arrives. See Streaming and Authentication.

Toolchain and Linux requirements

Every generated Swift package requires Swift 6.1 or newer (Xcode 16.3 or newer on Apple platforms). SDKs with binary downloads, WebSocket operations, GraphQL subscriptions or event channels also need the system zlib development library on Linux (zlib1g-dev on Debian/Ubuntu). Their Linux transport uses SwiftNIO and NIOSSL; Apple defaults use Foundation.

For Client binary-download and WebSocket methods, supplying session: on Linux also requires an explicit streamingTransport:. Choose NIOStreamingTransport() or implement StreamingTransport for custom policy. The built-in transport verifies TLS certificates and hostnames and accepts optional PEM trust roots and a client certificate/key; it does not follow redirects or supply cookie storage, caching, proxies or authentication-challenge handling. A supplied URLSession continues to serve ordinary requests, and its policy is not copied into the streaming transport. GraphQLTransport and EventTransport remain the custom-transport interfaces for their respective clients. The generated README includes the applicable configuration example.

Unknown response fields

Your API can add a response field without it being a breaking change — but a generated Codable struct decodes only its declared CodingKeys, so the key would be dropped. The generated models keep it instead: a field the SDK wasn’t generated from is retained on decode, readable through an accessor, and written back out when the model is re-serialized.

let pet = try await client.pets.createPet(body: body)

// A field your API started returning after this SDK was generated.
if let species = pet.extraFields["species"] {
    print(species)
}

// Re-encoding preserves it — a read-modify-write never silently drops it.
let json = try JSONEncoder().encode(pet)

extraFields is a let holding [String: JSONValue], so nested objects and arrays survive intact, and retention is recursive.

Your existing construction still compiles: the memberwise initializer keeps its original signature and starts the model with an empty bag.

The retained fields are read-only by design. To send a field your spec doesn’t model yet, use the per-call extra-body escape hatch rather than writing to the retained bag.

Missing and null fields

By default an optional property that decodes from JSON null is nil, the same as an omitted key, and encodeIfPresent leaves it out on re-encode — so a read-modify-write sends a decoded null back as an absent key. Set targets.swift.model_presence: explicit to keep missing, null and a value apart: declared properties become Field<Value> (.missing, .null, .value(_)). That changes property and constructor argument types; the generated README shows the idioms.

Binary downloads

Binary endpoints return an owned BinaryResponse with response metadata. readAll(maxBytes:) bounds delivered bytes; copy(to:) accepts a FileHandle or async sink closure. Use defer { download.close() } after opening an incremental reader. Mixed JSON/binary operations expose an explicit result branch. See file transfers.

File uploads

multipart/form-data operations build the multipart body from their fields for you; an application/octet-stream operation takes a positional body: Data argument sent raw with the right Content-Type (Data? when the body is optional, see below).

Optional request bodies

When your spec marks a request body optional (required: false), the argument is a defaulted optional: body: Model? = nil for a typed body, Bool? (or Int?, String?) for a scalar one, AnyCodable? for an inline object and Data? for an octet-stream one. Leave it off to send no body and no Content-Type, or pass a value to send it. A falsy value is still a value: false, 0, "" and AnyCodable(nil) are sent as written. A body whose schema admits null (marked nullable on the $ref or on the schema it names, or a union with a null member) is Model??: the outer nil leaves it out and .some(nil) sends JSON null. An optional multipart/form-data body makes every part optional: the parts you pass are the parts sent, and passing none sends no body. The same holds on paginated operations, where the body is re-sent unchanged on every later page. An operation whose config sets auto_populate body_fields is the one exception to “no body”: it sends just those constants (with their Content-Type) when you leave the body out. A required body stays a plain argument. On a paginated operation (the iterator, its Scoped companion and its Page companion) a required body is the body: argument, and the call without it that compiled before these methods sent bodies is kept as an overload that sends no body of yours, so existing client.items.list() call sites keep compiling; a body whose schema admits null is Model? there, where nil sends JSON null and the overload without body: leaves it out.

Keychain secure storage

Opt-in. The Keychain target is compile-verified against a real Apple toolchain (macOS) by the scheduled mobile-compile CI lane — the #if canImport(Security) Keychain store builds on a real Apple SDK. The Swift SDK above is unaffected.

Opt in with targets.swift.keychain: true in your glotto.yml to add a pluggable secure token store to the bearer client — the same native-mobile secure-storage seam the React Native SDK ships:

targets:
  swift:
    keychain: true

The client gains an optional tokenStore: SecureTokenStore? parameter; when supplied, the bearer token is read from it per request (falling back to the static token). A Keychain-backed implementation (KeychainTokenStore, over the Security framework SecItem generic-password API) is generated for you. SecureTokenStore is a plain protocol, so you can supply your own store too. The Keychain implementation is #if canImport(Security)-guarded, so the SDK still compiles on Linux (where the Security framework is absent) — exactly like the engine’s streaming Linux fallbacks.

Biometric-gated storage

Opt-in, experimental. targets.swift.keychainBiometric: true requires targets.swift.keychain: true and is compile-verified on the scheduled mobile-compile lane. The plain Keychain store above stays the default and byte-identical.

For credentials that must be released only after the user authenticates, add targets.swift.keychainBiometric: true:

targets:
  swift:
    keychain: true
    keychainBiometric: true

This additionally emits a BiometricKeychainTokenStore (alongside the plain KeychainTokenStore) whose Keychain item carries a SecAccessControlCreateWithFlags(.biometryCurrentSet) access control, so the OS presents the biometric prompt on access. An optional LAContext (LocalAuthentication-guarded) lets you reuse an existing authentication or customize the prompt. It rides the same #if canImport(Security) guard, so the SDK still compiles on Linux.

Connectivity-aware retries & lifecycle-aware backoff

Opt-in. The concrete monitors are compile-verified against a real Apple toolchain (macOS) by the scheduled mobile-compile CI lane — the #if canImport(Network) / #if canImport(UIKit) monitors build on a real Apple SDK. The Swift SDK above is unaffected.

The retry loop can be made network- and lifecycle-aware — the two remaining native-mobile seams the React Native and Kotlin-Android SDKs ship. They are two independent opt-in flags:

targets:
  swift:
    connectivity: true   # pause retries while the device is offline
    lifecycle: true      # pause backoff while the app is backgrounded

With connectivity on, the Client gains an optional connectivity: ConnectivityMonitor? parameter. Before each (re)try, a known-offline state is treated as transient — the client waits and re-checks rather than burning the request. A concrete NWPathMonitorConnectivity (over the Network framework) is generated for you.

With lifecycle on, the Client gains an optional lifecycle: AppLifecycle? parameter. While the app is backgrounded (where the OS freezes timers), the client pauses the backoff without consuming a retry attempt until the app returns to the foreground. A concrete UIApplicationLifecycle (over UIKit lifecycle notifications) is generated for you.

ConnectivityMonitor and AppLifecycle are plain protocols, so you can supply your own implementations too. Both concrete monitors are #if canImport(...)-guarded (Network / UIKit), so the SDK still compiles on Linux where those frameworks are absent — exactly like the Keychain store above.