Client SwiftUI RivetKit
Utilise cette skill lors de la création d'applications SwiftUI qui se connectent à des Rivet Actors avec RivetKitSwiftUI.
Version
Version RivetKit : 2.2.0
Politique de gestion des erreurs
- Préférer le comportement fail-fast par défaut.
- Éviter les blocs
do/catchgénériques sauf si absolument nécessaire. - Si un bloc catch est utilisé, gérer l'erreur explicitement, au minimum en la loggant.
Installation
Ajoute la dépendance du package Swift et importe RivetKitSwiftUI :
// 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 ré-exporte RivetKitClient et SwiftUI, donc un seul import suffit.
Client minimal
import RivetKitSwiftUI
import SwiftUI
@main
struct HelloWorldApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.rivetKit(endpoint: "https://my-namespace:pk_...@api.rivet.dev")
}
}
}
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
}
}
}
Options des actors
Le property wrapper @Actor utilise toujours la sémantique get-or-create et accepte :
name(obligatoire)keycommeStringou[String](obligatoire)params(paramètres de connexion optionnels)createWithInput(input de création optionnel)createInRegion(hint de création optionnel)enabled(bascule du cycle de vie de la connexion)
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
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)
}
}
}
}
S'abonner aux événements
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
}
}
}
Flux d'événements asynchrones
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)
}
}
}
}
Statut de connexion
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)
}
}
}
Gestion des erreurs
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
Clés
Les clés identifient de manière unique les instances d'actors. Utilise des clés composées (tableaux) pour un adressage hiérarchique :
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)")
}
}
Ne construis pas de clés avec interpolation de chaîne comme "org:\(userId)" quand userId contient des données utilisateur. Utilise plutôt des tableaux pour prévenir les attaques par injection de clé.
Configuration de l'environnement
Appelle .rivetKit(endpoint:) ou .rivetKit(client:) une fois à la racine de ton arborescence de vues :
// Avec une chaîne endpoint (recommandé pour la plupart des apps)
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.rivetKit(endpoint: "https://my-namespace:pk_...@api.rivet.dev")
}
}
}
// Avec un client personnalisé (pour une configuration avancée)
@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)
}
}
}
Lors de l'utilisation de .rivetKit(endpoint:), le client est créé une fois et mis en cache par endpoint. Lors de l'utilisation de .rivetKit(client:), stocke le client comme propriété sur App (pas à l'intérieur de body) puisque SwiftUI peut appeler body plusieurs fois.
Variables d'environnement
ClientConfig lit les valeurs optionnelles des variables d'environnement :
RIVET_NAMESPACE- Namespace (peut aussi être dans l'URL endpoint)RIVET_TOKEN- Token d'authentification (peut aussi être dans l'URL endpoint)RIVET_RUNNER- Nom du runner (par défaut"default")
L'endpoint est toujours obligatoire. Il n'y a pas d'endpoint par défaut.
Format de l'endpoint
Les endpoints supportent la syntaxe d'authentification par URL :
https://namespace:token@api.rivet.dev
Tu peux aussi passer l'endpoint sans authentification et fournir RIVET_NAMESPACE et RIVET_TOKEN séparément. Pour les déploiements serverless, définis l'endpoint sur l'URL /api/rivet de ton app. Voir Endpoints pour plus de détails.
Référence API
Property Wrapper
@Actor(name, key:, params:, createWithInput:, createInRegion:, enabled:)- Property wrapper SwiftUI pour les connexions d'actors
View Modifiers
.rivetKit(endpoint:)- Configurer le client avec une URL endpoint (crée un client en cache).rivetKit(client:)- Configurer le client avec une instance personnalisée.onActorEvent(actor, event) { ... }- S'abonner aux événements d'actors (supporte 0–5 args typés).onActorError(actor) { error in ... }- Gérer les erreurs d'actors
ActorObservable
actor.action(name, args..., as:)- Appel d'action asynchroneactor.send(name, args...)- Action fire-and-forgetactor.events(name, as:)- AsyncStream d'événements typésactor.connStatus- Statut de connexion actuelactor.isConnected- Si connectéactor.handle-ActorHandlesous-jacent (optionnel)actor.connection-ActorConnectionsous-jacente (optionnelle)actor.error- Erreur la plus récente (optionnelle)
Types
ActorConnStatus- Enum de statut de connexion (.idle,.connecting,.connected,.disconnected,.disposed)ActorError- Erreurs d'actors typées avecgroup,code,message,metadata
Besoin de plus que le client ?
Si tu as besoin de plus sur les Rivet Actors, les registries ou RivetKit côté serveur, ajoute la skill principale :
npx skills add rivet-dev/skills
Puis utilise la skill rivetkit pour des conseils sur le backend.