rivetkit-client-swiftui

Par rivet-dev · skills

Guide du client SwiftUI RivetKit. À utiliser pour les applications SwiftUI qui se connectent aux Rivet Actors avec RivetKitSwiftUI, `@Actor`, les modificateurs de vue rivetKit et les bindings SwiftUI.

npx skills add https://github.com/rivet-dev/skills --skill rivetkit-client-swiftui

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/catch gé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)
  • key comme String ou [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 asynchrone
  • actor.send(name, args...) - Action fire-and-forget
  • actor.events(name, as:) - AsyncStream d'événements typés
  • actor.connStatus - Statut de connexion actuel
  • actor.isConnected - Si connecté
  • actor.handle - ActorHandle sous-jacent (optionnel)
  • actor.connection - ActorConnection sous-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 avec group, 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.

Skills similaires