455 lines
11 KiB
Markdown
455 lines
11 KiB
Markdown
# Swift
|
||
|
||
> Source: `src/content/docs/clients/swift.mdx`
|
||
> Canonical URL: https://rivet.dev/docs/clients/swift
|
||
> Description: Connect Swift apps to Rivet Actors.
|
||
|
||
---
|
||
## Install
|
||
|
||
Add the Swift package dependency and import `RivetKitClient`:
|
||
|
||
```swift
|
||
// Package.swift
|
||
dependencies: [
|
||
.package(url: "https://github.com/rivet-dev/rivetkit-swift", from: "2.0.0")
|
||
]
|
||
|
||
targets: [
|
||
.target(
|
||
name: "MyApp",
|
||
dependencies: [
|
||
.product(name: "RivetKitClient", package: "rivetkit-swift")
|
||
]
|
||
)
|
||
]
|
||
```
|
||
|
||
## Minimal Client
|
||
|
||
### Endpoint URL
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(
|
||
endpoint: "https://my-namespace:pk_...@api.rivet.dev"
|
||
)
|
||
let client = RivetKitClient(config: config)
|
||
|
||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||
let count: Int = try await handle.action("increment", 1, as: Int.self)
|
||
```
|
||
|
||
### Explicit Fields
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(
|
||
endpoint: "https://api.rivet.dev",
|
||
namespace: "my-namespace",
|
||
token: "pk_..."
|
||
)
|
||
let client = RivetKitClient(config: config)
|
||
|
||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||
let count: Int = try await handle.action("increment", 1, as: Int.self)
|
||
```
|
||
|
||
## Stateless vs Stateful
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
|
||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||
|
||
// Stateless: each call is independent
|
||
let current: Int = try await handle.action("getCount", as: Int.self)
|
||
print("Current count: \(current)")
|
||
|
||
// Stateful: keep a connection open for realtime events
|
||
let conn = handle.connect()
|
||
|
||
// Subscribe to events using AsyncStream
|
||
let eventTask = Task {
|
||
for await count in await conn.events("count", as: Int.self) {
|
||
print("Event: \(count)")
|
||
}
|
||
}
|
||
|
||
_ = try await conn.action("increment", 1, as: Int.self)
|
||
|
||
eventTask.cancel()
|
||
await conn.dispose()
|
||
await client.dispose()
|
||
```
|
||
|
||
## Getting Actors
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
struct GameInput: Encodable {
|
||
let mode: String
|
||
}
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
|
||
// Get or create an actor
|
||
let room = client.getOrCreate("chatRoom", ["room-42"])
|
||
|
||
// Get an existing actor (fails if not found)
|
||
let existing = client.get("chatRoom", ["room-42"])
|
||
|
||
// Create a new actor with input
|
||
let created = try await client.create(
|
||
"game",
|
||
["game-1"],
|
||
options: CreateOptions(input: GameInput(mode: "ranked"))
|
||
)
|
||
|
||
// Get actor by ID
|
||
let byId = client.getForId("chatRoom", "actor-id")
|
||
|
||
// Resolve actor ID
|
||
let resolvedId = try await room.resolve()
|
||
print("Resolved ID: \(resolvedId)")
|
||
|
||
await client.dispose()
|
||
```
|
||
|
||
Actions support positional overloads for 0–5 args:
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||
|
||
let count: Int = try await handle.action("getCount")
|
||
let updated: String = try await handle.action("rename", "new-name")
|
||
let ok: Bool = try await handle.action("setScore", "user-1", 42)
|
||
|
||
print("Count: \(count), Updated: \(updated), OK: \(ok)")
|
||
await client.dispose()
|
||
```
|
||
|
||
If you need more than 5 arguments, use the raw JSON fallback:
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
let handle = client.getOrCreate("counter", ["my-counter"])
|
||
|
||
let args: [JSONValue] = [
|
||
.string("user-1"),
|
||
.number(.int(42)),
|
||
.string("extra"),
|
||
.string("more"),
|
||
.string("args"),
|
||
.string("here")
|
||
]
|
||
let ok: Bool = try await handle.action("setScore", args: args, as: Bool.self)
|
||
print("OK: \(ok)")
|
||
|
||
await client.dispose()
|
||
```
|
||
|
||
## Connection Parameters
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
struct ConnParams: Encodable {
|
||
let authToken: String
|
||
}
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
|
||
let chat = client.getOrCreate(
|
||
"chatRoom",
|
||
["general"],
|
||
options: GetOrCreateOptions(params: ConnParams(authToken: "jwt-token-here"))
|
||
)
|
||
|
||
let conn = chat.connect()
|
||
|
||
// Use the connection...
|
||
for await status in await conn.statusChanges() {
|
||
print("Status: \(status.rawValue)")
|
||
if status == .connected {
|
||
break
|
||
}
|
||
}
|
||
|
||
await conn.dispose()
|
||
await client.dispose()
|
||
```
|
||
|
||
## Subscribing to Events
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
let conn = client.getOrCreate("chatRoom", ["general"]).connect()
|
||
|
||
// Subscribe to events using AsyncStream
|
||
let messageTask = Task {
|
||
for await (from, body) in await conn.events("message", as: (String, String).self) {
|
||
print("\(from): \(body)")
|
||
}
|
||
}
|
||
|
||
// For one-time events, break after receiving
|
||
let gameOverTask = Task {
|
||
for await _ in await conn.events("gameOver", as: Void.self) {
|
||
print("done")
|
||
break
|
||
}
|
||
}
|
||
|
||
// Let it run for a bit
|
||
try await Task.sleep(for: .seconds(5))
|
||
|
||
// Cancel when done
|
||
messageTask.cancel()
|
||
gameOverTask.cancel()
|
||
await conn.dispose()
|
||
await client.dispose()
|
||
```
|
||
|
||
Event streams support 0–5 typed arguments. If you need raw values or more than 5 arguments, use `JSONValue`:
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
let conn = client.getOrCreate("chatRoom", ["general"]).connect()
|
||
|
||
let rawTask = Task {
|
||
for await args in await conn.events("message") {
|
||
print(args)
|
||
}
|
||
}
|
||
|
||
try await Task.sleep(for: .seconds(5))
|
||
rawTask.cancel()
|
||
await conn.dispose()
|
||
await client.dispose()
|
||
```
|
||
|
||
## Connection Lifecycle
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
let conn = client.getOrCreate("chatRoom", ["general"]).connect()
|
||
|
||
// Monitor status changes (immediately yields current status)
|
||
let statusTask = Task {
|
||
for await status in await conn.statusChanges() {
|
||
print("status: \(status.rawValue)")
|
||
}
|
||
}
|
||
|
||
// Monitor errors
|
||
let errorTask = Task {
|
||
for await error in await conn.errors() {
|
||
print("error: \(error.group).\(error.code)")
|
||
}
|
||
}
|
||
|
||
// Monitor open/close events
|
||
let openTask = Task {
|
||
for await _ in await conn.opens() {
|
||
print("connected")
|
||
}
|
||
}
|
||
|
||
let closeTask = Task {
|
||
for await _ in await conn.closes() {
|
||
print("disconnected")
|
||
}
|
||
}
|
||
|
||
// Check current status
|
||
let current = await conn.currentStatus
|
||
print("Current status: \(current.rawValue)")
|
||
|
||
// Let it run for a bit
|
||
try await Task.sleep(for: .seconds(5))
|
||
|
||
// Cleanup
|
||
statusTask.cancel()
|
||
errorTask.cancel()
|
||
openTask.cancel()
|
||
closeTask.cancel()
|
||
await conn.dispose()
|
||
await client.dispose()
|
||
```
|
||
|
||
## Low-Level HTTP & WebSocket
|
||
|
||
For actors that implement `onRequest` or `onWebSocket`, you can call them directly:
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
let handle = client.getOrCreate("chatRoom", ["general"])
|
||
|
||
// Raw HTTP request
|
||
let response = try await handle.fetch("history")
|
||
let history: [String] = try response.json([String].self)
|
||
print("History: \(history)")
|
||
|
||
// Raw WebSocket connection
|
||
let websocket = try await handle.websocket(path: "stream")
|
||
try await websocket.send(text: "hello")
|
||
let message = try await websocket.receive()
|
||
print("Received: \(message)")
|
||
|
||
await client.dispose()
|
||
```
|
||
|
||
## Calling from Backend
|
||
|
||
Use the same client in server-side Swift (Vapor, Hummingbird, etc.):
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
|
||
let handle = client.getOrCreate("counter", ["server-counter"])
|
||
let count: Int = try await handle.action("increment", 1, as: Int.self)
|
||
print("Count: \(count)")
|
||
|
||
await client.dispose()
|
||
```
|
||
|
||
## Error Handling
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
|
||
do {
|
||
_ = try await client.getOrCreate("user", ["user-123"])
|
||
.action("updateUsername", "ab", as: String.self)
|
||
} catch let error as ActorError {
|
||
print("Error code: \(error.code)")
|
||
print("Metadata: \(String(describing: error.metadata))")
|
||
}
|
||
|
||
await client.dispose()
|
||
```
|
||
|
||
If you need an untyped response, you can decode to `JSONValue`:
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
let handle = client.getOrCreate("data", ["raw"])
|
||
|
||
let value: JSONValue = try await handle.action("getRawPayload")
|
||
print("Raw value: \(value)")
|
||
|
||
await client.dispose()
|
||
```
|
||
|
||
## Concepts
|
||
|
||
### Keys
|
||
|
||
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
|
||
|
||
```swift
|
||
import RivetKitClient
|
||
|
||
let config = try ClientConfig(endpoint: "http://localhost:6420")
|
||
let client = RivetKitClient(config: config)
|
||
|
||
// Use compound keys for hierarchical addressing
|
||
let room = client.getOrCreate("chatRoom", ["org-acme", "general"])
|
||
let actorId = try await room.resolve()
|
||
print("Actor ID: \(actorId)")
|
||
|
||
await client.dispose()
|
||
```
|
||
|
||
Don't build keys with string interpolation like `"org:\(userId)"` when `userId` contains user data. Use arrays instead to prevent key injection attacks.
|
||
|
||
### Environment Variables
|
||
|
||
`ClientConfig` reads optional values from environment variables:
|
||
|
||
- `RIVET_NAMESPACE` - Namespace (can also be in endpoint URL)
|
||
- `RIVET_TOKEN` - Authentication token (can also be in endpoint URL)
|
||
- `RIVET_RUNNER` - Runner name (defaults to `"default"`)
|
||
|
||
The `endpoint` parameter is always required. There is no default endpoint.
|
||
|
||
### Endpoint Format
|
||
|
||
Endpoints support URL auth syntax:
|
||
|
||
```
|
||
https://namespace:token@api.rivet.dev
|
||
```
|
||
|
||
You can also pass the endpoint without auth and provide `RIVET_NAMESPACE` and `RIVET_TOKEN` separately. For serverless deployments, set the endpoint to your app's `/api/rivet` URL. See [Endpoints](/docs/general/endpoints#url-auth-syntax) for details.
|
||
|
||
## API Reference
|
||
|
||
### Client
|
||
- `RivetKitClient(config:)` - Create a client with a config
|
||
- `ClientConfig` - Configure endpoint, namespace, and token
|
||
- `client.get()` / `getOrCreate()` / `getForId()` / `create()` - Get actor handles
|
||
- `client.dispose()` - Dispose the client and all connections
|
||
|
||
### ActorHandle
|
||
- `handle.action(name, args..., as:)` - Stateless action call
|
||
- `handle.connect()` - Create a stateful connection
|
||
- `handle.resolve()` - Get the actor ID
|
||
- `handle.getGatewayUrl()` - Get the raw gateway URL
|
||
- `handle.fetch(path, request:)` - Raw HTTP request
|
||
- `handle.websocket(path:)` - Raw WebSocket connection
|
||
|
||
### ActorConnection
|
||
- `conn.action(name, args..., as:)` - Action call over WebSocket
|
||
- `conn.events(name, as:)` - AsyncStream of typed events
|
||
- `conn.statusChanges()` - AsyncStream of status changes
|
||
- `conn.errors()` - AsyncStream of connection errors
|
||
- `conn.opens()` - AsyncStream that yields on connection open
|
||
- `conn.closes()` - AsyncStream that yields on connection close
|
||
- `conn.currentStatus` - Current connection status
|
||
- `conn.dispose()` - Close the connection
|
||
|
||
### Types
|
||
- `ActorConnStatus` - Connection status enum (`.idle`, `.connecting`, `.connected`, `.disconnected`, `.disposed`)
|
||
- `ActorError` - Typed actor errors with `group`, `code`, `message`, `metadata`
|
||
- `JSONValue` - Raw JSON value for untyped responses
|
||
|
||
_Source doc path: /docs/clients/swift_
|