RivetKit SwiftUI client guidance. Use for SwiftUI apps that connect to Rivet Actors with RivetKitSwiftUI, @Actor, rivetKit view modifiers, and SwiftUI bindings.
---
name: "rivetkit-client-swiftui"
description: "RivetKit SwiftUI client guidance. Use for SwiftUI apps that connect to Rivet Actors with RivetKitSwiftUI, @Actor, rivetKit view modifiers, and SwiftUI bindings."
---
# RivetKit SwiftUI Client
Use this skill when building SwiftUI apps that connect to Rivet Actors with `RivetKitSwiftUI`.
## Version
RivetKit version: 2.3.0-rc.5
## Error Handling Policy
- Prefer fail-fast behavior by default.
- Avoid broad `do/catch` unless absolutely needed.
- If a catch block is used, handle the error explicitly, at minimum by logging it.
## Install
Add the Swift package dependency and import `RivetKitSwiftUI`:
```swift
// Package.swift
dependencies: [
.package(url: "https://github.com/rivet-dev/rivetkit-swift", from: "2.0.0")
]
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "RivetKitSwiftUI", package: "rivetkit-swift")
]
)
]
```
`RivetKitSwiftUI` re-exports `RivetKitClient` and `SwiftUI`, so a single import covers both.
## Minimal Client
```swift HelloWorldApp.swift
import RivetKitSwiftUI
import SwiftUI
@main
struct HelloWorldApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.rivetKit(endpoint: "https://my-namespace:pk_...@api.rivet.dev")
}
}
}
```
```swift ContentView.swift
import RivetKitSwiftUI
import SwiftUI
struct ContentView: View {
@Actor("counter", key: ["my-counter"]) private var counter
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("\(count)")
.font(.system(size: 64, weight: .bold, design: .rounded))
Button("Increment") {
counter.send("increment", 1)
}
.disabled(!counter.isConnected)
}
.task {
count = (try? await counter.action("getCount")) ?? 0
}
.onActorEvent(counter, "newCount") { (newCount: Int) in
count = newCount
}
}
}
```
## Actor Options
The `@Actor` property wrapper always uses get-or-create semantics and accepts:
- `name` (required)
- `key` as `String` or `[String]` (required)
- `params` (optional connection parameters)
- `createWithInput` (optional creation input)
- `createInRegion` (optional creation hint)
- `enabled` (toggle connection lifecycle)
```swift
import RivetKitSwiftUI
import SwiftUI
struct ConnParams: Encodable {
let authToken: String
}
struct ChatView: View {
@Actor(
"chatRoom",
key: ["general"],
params: ConnParams(authToken: "jwt-token"),
enabled: true
) private var chat
var body: some View {
Text("Chat: \(chat.connStatus.rawValue)")
}
}
```
## Actions
```swift
import RivetKitSwiftUI
import SwiftUI
struct CounterView: View {
@Actor("counter", key: ["my-counter"]) private var counter
@State private var count = 0
@State private var name = ""
var body: some View {
VStack {
Text("Count: \(count)")
Text("Name: \(name)")
Button("Fetch") {
Task {
count = try await counter.action("getCount")
name = try await counter.action("rename", "new-name")
}
}
Button("Increment") {
counter.send("increment", 1)
}
}
}
}
```
## Subscribing to Events
```swift
import RivetKitSwiftUI
import SwiftUI
struct GameView: View {
@Actor("game", key: ["game-1"]) private var game
@State private var count = 0
@State private var isGameOver = false
var body: some View {
VStack {
Text("Count: \(count)")
if isGameOver {
Text("Game Over!")
}
}
.onActorEvent(game, "newCount") { (newCount: Int) in
count = newCount
}
.onActorEvent(game, "gameOver") {
isGameOver = true
}
}
}
```
## Async Event Streams
```swift
import RivetKitSwiftUI
import SwiftUI
struct ChatView: View {
@Actor("chatRoom", key: ["general"]) private var chat
@State private var messages: [String] = []
var body: some View {
List(messages, id: \.self) { message in
Text(message)
}
.task {
for await message in chat.events("message", as: String.self) {
messages.append(message)
}
}
}
}
```
## Connection Status
```swift
import RivetKitSwiftUI
import SwiftUI
struct StatusView: View {
@Actor("counter", key: ["my-counter"]) private var counter
@State private var count = 0
var body: some View {
VStack {
Text("Status: \(counter.connStatus.rawValue)")
if counter.connStatus == .connected {
Text("Connected!")
.foregroundStyle(.green)
}
Button("Fetch via Handle") {
Task {
if let handle = counter.handle {
count = try await handle.action("getCount", as: Int.self)
}
}
}
.disabled(!counter.isConnected)
}
}
}
```
## Error Handling
```swift
import RivetKitSwiftUI
import SwiftUI
struct UserView: View {
@Actor("user", key: ["user-123"]) private var user
@State private var errorMessage: String?
@State private var username = ""
var body: some View {
VStack {
TextField("Username", text: $username)
Button("Update Username") {
Task {
do {
let _: String = try await user.action("updateUsername", username)
} catch let error as ActorError {
errorMessage = "\(error.code): \(String(describing: error.metadata))"
}
}
}
if let errorMessage {
Text(errorMessage)
.foregroundStyle(.red)
}
}
.onActorError(user) { error in
errorMessage = "\(error.group).\(error.code): \(error.message)"
}
}
}
```
## Concepts
### Keys
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
```swift
import RivetKitSwiftUI
import SwiftUI
struct OrgChatView: View {
@Actor("chatRoom", key: ["org-acme", "general"]) private var room
var body: some View {
Text("Room: \(room.connStatus.rawValue)")
}
}
```
Don't build keys with string interpolation like `"org:\(userId)"` when `userId` contains user data. Use arrays instead to prevent key injection attacks.
### Environment Configuration
Call `.rivetKit(endpoint:)` or `.rivetKit(client:)` once at the root of your view tree:
```swift
// With endpoint string (recommended for most apps)
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.rivetKit(endpoint: "https://my-namespace:pk_...@api.rivet.dev")
}
}
}
// With custom client (for advanced configuration)
@main
struct MyApp: App {
private let client = RivetKitClient(
config: try! ClientConfig(endpoint: "https://api.rivet.dev", token: "pk_...")
)
var body: some Scene {
WindowGroup {
ContentView()
.rivetKit(client: client)
}
}
}
```
When using `.rivetKit(endpoint:)`, the client is created once and cached per endpoint. When using `.rivetKit(client:)`, store the client as a property on `App` (not inside `body`) since SwiftUI can call `body` multiple times.
### 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 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
### Property Wrapper
- `@Actor(name, key:, params:, createWithInput:, createInRegion:, enabled:)` - SwiftUI property wrapper for actor connections
### View Modifiers
- `.rivetKit(endpoint:)` - Configure client with an endpoint URL (creates cached client)
- `.rivetKit(client:)` - Configure client with a custom instance
- `.onActorEvent(actor, event) { ... }` - Subscribe to actor events (supports 0–5 typed args)
- `.onActorError(actor) { error in ... }` - Handle actor errors
### ActorObservable
- `actor.action(name, args..., as:)` - Async action call
- `actor.send(name, args...)` - Fire-and-forget action
- `actor.events(name, as:)` - AsyncStream of typed events
- `actor.connStatus` - Current connection status
- `actor.isConnected` - Whether connected
- `actor.handle` - Underlying `ActorHandle` (optional)
- `actor.connection` - Underlying `ActorConnection` (optional)
- `actor.error` - Most recent error (optional)
### Types
- `ActorConnStatus` - Connection status enum (`.idle`, `.connecting`, `.connected`, `.disconnected`, `.disposed`)
- `ActorError` - Typed actor errors with `group`, `code`, `message`, `metadata`
## Need More Than the Client?
If you need more about Rivet Actors, registries, or server-side RivetKit, add the main skill:
```bash
npx skills add rivet-dev/skills
```
Then use the `rivetkit` skill for backend guidance.
Creator's repository · rivet-dev/skills