--- 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.0.42-rc.1 ## 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 {{"title":"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 {{"title":"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.