rivetkit-swift/SPEC_SWIFTUI.md
SwiftUI-compatible API for RivetKit, aligned with React useActor / useEvent behavior:
enabled@PropertyWrapper, view modifiers)SwiftUI accepts configuration the same way React’s createRivetKit does: endpoint string or full config.
ContentView()
.rivetKit("https://example.com/api/rivet")
// or
ContentView()
.rivetKit(ClientConfig(endpoint: "...", token: "...", namespace: "..."))
extension View {
func rivetKit(_ endpoint: String) -> some View
func rivetKit(_ config: ClientConfig) -> some View
}
RivetKitClient in SwiftUI environment.@Actor reads the client from environment. If none is set, it uses a default ClientConfig().onActorError using ActorError(group: "client", code: "config_error", ...).@Actor("counter", key: ["my-counter"]) var counter
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Actor name from registry |
key | String or [String] | Yes | Actor instance key |
params | Encodable? | No | Connection parameters |
createWithInput | Encodable? | No | Input for actor creation |
createInRegion | String? | No | Region hint for creation |
enabled | Bool | No | Whether the actor is active (default true) |
Behavior (React parity):
enabled == false, any active connection is disposed and state resets to idle.connect() / disconnect() API.Exposed Properties:
actor.connStatus // ActorConnStatus (idle, connecting, connected, disconnected)
actor.error // ActorError? (connection + decode errors)
actor.connection // ActorConnection? (nil until connected)
actor.handle // ActorHandle? (stateless handle for HTTP actions)
actor.hash // String (stable identity hash)
actor.opts // ActorOptions (normalized inputs)
actor.isConnected // Bool (derived: connStatus == .connected)
public enum ActorConnStatus: String, Sendable {
case idle
case connecting
case connected
case disconnected
}
Async actions match React behavior. Action errors are delivered via throws (not onActorError).
let count: Int = try await actor.action("getCount")
let user: User = try await actor.action("getUser", userId)
Overloads:
func action<R: Decodable>(_ name: String) async throws -> R
func action<A: Encodable, R: Decodable>(_ name: String, _ a: A) async throws -> R
func action<A: Encodable, B: Encodable, R: Decodable>(_ name: String, _ a: A, _ b: B) async throws -> R
func action<A: Encodable, B: Encodable, C: Encodable, R: Decodable>(_ name: String, _ a: A, _ b: B, _ c: C) async throws -> R
func action<R: Decodable>(_ name: String, args: [any Encodable]) async throws -> R
Mirror JS by ignoring the async result.
Button("+") {
counter.send("increment", 1)
}
Overloads:
func send(_ name: String)
func send<A: Encodable>(_ name: String, _ a: A)
func send<A: Encodable, B: Encodable>(_ name: String, _ a: A, _ b: B)
func send<A: Encodable, B: Encodable, C: Encodable>(_ name: String, _ a: A, _ b: B, _ c: C)
func send(_ name: String, args: [any Encodable])
Behavior:
Task { _ = try? await action(...) }.onActorError)..onActorEvent(actor, "newCount") { (count: Int) in
self.count = count
}
Overloads (positional args):
.onActorEvent(actor, "tick") { () in }
.onActorEvent(actor, "newCount") { (count: Int) in }
.onActorEvent(actor, "move") { (x: Double, y: Double) in }
.onActorEvent(actor, "triple") { (a: String, b: Int, c: Bool) in }
// Raw (deprecated) - receives all args
.onActorEvent(actor, "event") { args in }
Decoding Rules:
onActorError using ActorError(group: "client", code: "decode_error", ...)..task {
for await message in actor.events("message", as: Message.self) {
messages.append(message)
}
}
.onActorError(counter) { error in
showAlert(error.message)
}
onActorEvent overloads.@State private var isEnabled = true
@Actor("chat", key: "room-1", enabled: isEnabled) var chat
var body: some View {
Toggle("Connected", isOn: $isEnabled)
}
enabled == false disposes the connection and resets status to .idle.enabled == true reconnects.@MainActor
public final class ActorObservable: ObservableObject {
@Published public private(set) var connStatus: ActorConnStatus
@Published public private(set) var error: ActorError?
@Published public private(set) var connection: ActorConnection?
@Published public private(set) var handle: ActorHandle?
@Published public private(set) var hash: String
@Published public private(set) var opts: ActorOptions
public var isConnected: Bool { connStatus == .connected }
// Async actions
public func action<R: Decodable>(_ name: String) async throws -> R
public func action<A: Encodable, R: Decodable>(_ name: String, _ a: A) async throws -> R
public func action<A: Encodable, B: Encodable, R: Decodable>(_ name: String, _ a: A, _ b: B) async throws -> R
public func action<A: Encodable, B: Encodable, C: Encodable, R: Decodable>(_ name: String, _ a: A, _ b: B, _ c: C) async throws -> R
public func action<R: Decodable>(_ name: String, args: [any Encodable]) async throws -> R
// Fire-and-forget
public func send(_ name: String)
public func send<A: Encodable>(_ name: String, _ a: A)
public func send<A: Encodable, B: Encodable>(_ name: String, _ a: A, _ b: B)
public func send<A: Encodable, B: Encodable, C: Encodable>(_ name: String, _ a: A, _ b: B, _ c: C)
public func send(_ name: String, args: [any Encodable])
// Event streams
public func events<T: Decodable>(_ name: String, as: T.Type) -> AsyncStream<T>
public func events<T: Decodable & Sendable>(_ name: String, as: T.Type = T.self) -> AsyncStream<T>
public func events(_ name: String, as: Void.Type = Void.self) -> AsyncStream<Void>
public func events<A: Decodable & Sendable, B: Decodable & Sendable>(_ name: String, as: (A, B).Type) -> AsyncStream<(A, B)>
public func events<A: Decodable & Sendable, B: Decodable & Sendable, C: Decodable & Sendable>(_ name: String, as: (A, B, C).Type) -> AsyncStream<(A, B, C)>
@available(*, deprecated)
public func events(_ name: String) -> AsyncStream<[JSONValue]>
}
extension View {
func onActorEvent(
_ actor: ActorObservable,
_ event: String,
perform: @escaping () -> Void
) -> some View
func onActorEvent<A: Decodable>(
_ actor: ActorObservable,
_ event: String,
perform: @escaping (A) -> Void
) -> some View
func onActorEvent<A: Decodable, B: Decodable>(
_ actor: ActorObservable,
_ event: String,
perform: @escaping (A, B) -> Void
) -> some View
func onActorEvent<A: Decodable, B: Decodable, C: Decodable>(
_ actor: ActorObservable,
_ event: String,
perform: @escaping (A, B, C) -> Void
) -> some View
func onActorEvent(
_ actor: ActorObservable,
_ event: String,
perform: @escaping ([JSONValue]) -> Void
) -> some View
func onActorError(
_ actor: ActorObservable,
perform: @escaping (ActorError) -> Void
) -> some View
}
@propertyWrapper
public struct Actor: DynamicProperty {
public init(
_ name: String,
key: String,
params: (any Encodable)? = nil,
createWithInput: (any Encodable)? = nil,
createInRegion: String? = nil,
enabled: Bool = true
)
public init(
_ name: String,
key: [String],
params: (any Encodable)? = nil,
createWithInput: (any Encodable)? = nil,
createInRegion: String? = nil,
enabled: Bool = true
)
public var wrappedValue: ActorObservable { get }
public var projectedValue: ActorObservable { get }
}