Data Binding de Rive en SwiftUI (nuevo runtime de Apple)
Por el equipo de Charmotion · Actualizado el

Para usar Data Binding de Rive en SwiftUI, crea un Worker y un File, genera una instancia de view model con file.createViewModelInstance, vincúlala con artboard.createStateMachine(name, binding: instance) y renderiza Rive(file:artboard:stateMachine:) mediante RiveUIViewRepresentable(rive:). Conserva la instancia y luego llama a setValue(of:to:) y fire(trigger:) sobre ella.
Puntos clave
- El nuevo runtime de Apple llegó en rive-ios 6.24.0 y usa Swift Concurrency; las llamadas de configuración son
asyncy todas las llamadas se ejecutan en el main actor. - No tiene inputs de máquina de estados. Los números, booleanos y triggers viven en un view model que vinculas de forma explícita.
- Conserva tú mismo la
ViewModelInstance: la máquina de estados no tiene un getter para las instancias vinculadas. NumberProperty.ValueesFloat, así que convierte un moodIntconFloat(mood).- No uses el inicializador obsoleto
Rive(file:…dataBind:)en código nuevo.
¿Qué cambió en el nuevo runtime de Rive para Apple?
El paquete de Rive para Apple (RiveRuntime, de rive-app/rive-ios) ahora contiene dos API. El runtime heredado se basa en RiveViewModel y, en palabras de Rive, "actualmente funciona en un solo hilo, el hilo principal". El nuevo runtime, introducido en la versión 6.24.0, es "una API pensada primero para Swift que aprovecha Swift Concurrency, con mejor compatibilidad multihilo". Al momento de escribir esto (octubre de 2026), la versión más reciente es la 6.28.1; la 6.28.0 agregó view models globales y Data Binding de blobs.
En la práctica, importan tres diferencias:
- Todo es asíncrono. Crear un worker, un archivo, un artboard, una máquina de estados y el objeto
Riverequieretry await. Rive sigue exigiendo que las llamadas se hagan en el main actor; el trabajo pesado ocurre en el hilo en segundo plano del worker. - Sin inputs, eventos ni animaciones lineales. La guía de migración indica que los inputs de máquina de estados y los listeners de eventos son exclusivos del runtime heredado, y que "no hay planes actuales de reintroducirlos". Las animaciones lineales deben envolverse en una máquina de estados.
- El Data Binding es el contrato. Tu código se comunica con una instancia de view model mediante propiedades tipadas (
NumberProperty,TriggerProperty, etc.) a las que se accede por ruta.
Si tu .riv solo expone inputs, quédate en el runtime heredado (consulta Rive en SwiftUI) o usa antes la opción del editor Menu → Convert Inputs to ViewModels.
¿Cómo se relacionan Worker, File, Artboard y Rive?
El nuevo runtime reparte lo que antes hacía RiveViewModel en cinco objetos. Cada uno se crea a partir del anterior:
| Objeto | Se crea con | Trabajo |
|---|---|---|
Worker | try await Worker() | Gestiona el hilo en segundo plano y los recursos compartidos fuera de banda (imágenes, fuentes, audio). |
File | try await File(source:worker:) | El .riv ya analizado. Las fuentes son .local(name, bundle), .url(URL) o .data(Data). |
Artboard | try await file.createArtboard() | El lienzo que se dibuja; pasa un nombre para usar un artboard que no sea el predeterminado. |
StateMachine | try await artboard.createStateMachine(name, binding: instance) | Ejecuta la lógica y lee la instancia del view model vinculada. |
Rive | try await Rive(file:artboard:stateMachine:) | La configuración de la vista (fit, fondo) que se entrega a la vista. |
La documentación de Rive indica que un Worker "debe permanecer activo mientras se use Rive" y que un File mantiene una referencia fuerte a su worker. Un worker equivale aproximadamente a un hilo en segundo plano; para varios gráficos pesados puedes darle a cada archivo su propio worker, o compartir un mismo worker para que los archivos compartan los recursos registrados.
Fíjate en el inicializador: el código fuente expone Worker() y Worker(configuration:). Algunos fragmentos de migración muestran Worker.shared(), que es un helper a nivel de app del ejemplo de Rive y no forma parte de RiveRuntime.
¿Cómo creo una instancia de view model y la vinculo a una máquina de estados?
Crea la instancia a partir del archivo y luego pásala a createStateMachine(_:binding:). .viewModelDefault(from: .artboardDefault(artboard)) significa "la instancia predeterminada del view model asignado a este artboard". Otras fuentes son .blank(from:), y .name(_:from:) para una instancia con nombre; ViewModelSource.name("Character") selecciona un view model por su nombre.
Coloca la configuración en un modelo observable para que la instancia sobreviva al body de la vista:
import SwiftUI
import RiveRuntime
@MainActor
final class CharacterModel: ObservableObject {
@Published private(set) var rive: Rive?
private var instance: ViewModelInstance?
private let mood = NumberProperty(path: "mood")
func load() async {
guard rive == nil else { return }
do {
let worker = try await Worker()
let file = try await File(source: .local("character", Bundle.main), worker: worker)
let artboard = try await file.createArtboard()
let instance = try await file.createViewModelInstance(
.viewModelDefault(from: .artboardDefault(artboard))
)
let stateMachine = try await artboard.createStateMachine(
"CharacterViewModelMachine",
binding: instance
)
self.instance = instance
rive = try await Rive(file: file, artboard: artboard, stateMachine: stateMachine)
} catch {
print("Rive setup failed: \(error)")
}
}
func setMood(_ value: Int) {
instance?.setValue(of: mood, to: Float(value))
}
func fire(_ trigger: String) {
instance?.fire(trigger: TriggerProperty(path: trigger))
}
}
struct CharacterView: View {
@StateObject private var model = CharacterModel()
var body: some View {
RiveUIViewRepresentable(rive: model.rive)
.frame(width: 240, height: 240)
.accessibilityLabel("Animated character")
.task { await model.load() }
}
}
RiveUIViewRepresentable recibe un Rive opcional, así que la vista puede aparecer antes de que termine la carga. En el código fuente el inicializador lleva etiqueta (rive:), aunque algunos fragmentos de la documentación la omiten. Si prefieres mantener la carga dentro de la vista, AsyncRiveUIViewRepresentable { … } recibe un closure asíncrono que devuelve un Rive; en ese caso necesitas otra forma de conservar la instancia.
Cuando puedas, define los valores iniciales antes de presentar la vista. La guía de migración de Rive señala que así se evita un fotograma que muestre los valores predeterminados del archivo.
¿Cómo defino el valor numérico de mood y disparo triggers desde SwiftUI?
Asignar una propiedad pone el cambio en cola y regresa de inmediato; a diferencia del runtime anterior, no llamas a play() después. Los triggers no tienen valor, así que los disparas:
struct UploadScreen: View {
@StateObject private var character = CharacterModel()
var body: some View {
VStack(spacing: 24) {
RiveUIViewRepresentable(rive: character.rive)
.frame(width: 220, height: 220)
Button("Upload") { Task { await upload() } }
}
.task { await character.load() }
}
private func upload() async {
character.setMood(5) // loading loop
let ok = await performUpload()
character.fire(ok ? "success" : "error") // one-shot reaction
character.setMood(0) // back to idle
}
}
Como la máquina de estados controla las transiciones, tu código solo indica lo que está haciendo la app. Un archivo bien construido deja que la reacción puntual de un trigger termine y luego vuelve al loop que mood tenga seleccionado en ese momento.
Los archivos de Charmotion usan exactamente este contrato: un view model Character en CharacterViewModelMachine, con estos valores. Cada set de un plan de pago también incluye un {Name}NewRuntimeView.swift listo para este runtime y un {Name}View.swift para el RiveViewModel legacy.
mood | Loop | mood | Loop |
|---|---|---|---|
| 0 | idle | 5 | loading |
| 1 | happy | 6 | listening |
| 2 | wave | 7 | talking |
| 3 | thinking | 8 | sleeping |
| 4 | sad | 9 | attention |
Triggers: success, error, yes, no, enter, exit.

¿Cómo observo un cambio de propiedad que llega desde Rive?
Data Binding es bidireccional: la máquina de estados puede cambiar una propiedad, por ejemplo cuando el usuario toca una zona interactiva del artboard. Las propiedades de tipo string, number, boolean, color y enum exponen valueStream(of:); los triggers exponen stream(of:), que emite Void cada vez que se dispara el trigger. Para leer una sola vez se usa try await instance.value(of:).
extension CharacterModel {
func watchMood() async {
guard let instance else { return }
do {
for try await value in instance.valueStream(of: NumberProperty(path: "mood")) {
print("mood is now \(Int(value))")
}
} catch {
print("Stream ended: \(error)")
}
}
func waitForSuccess() async throws {
guard let instance else { return }
for try await _ in instance.stream(of: TriggerProperty(path: "success")) {
break // first fire is enough
}
}
}
Inicia estos loops desde .task para que SwiftUI los cancele cuando la vista desaparezca. La documentación de Rive indica que el error lanzado es un ViewModelInstanceError. No necesitas el obsoleto dirtyStream() para forzar el redibujado; el controlador redibuja por sí solo después de los cambios en las instancias vinculadas.
El nuevo runtime no tiene un callback de nombre de estado como el RiveStateDelegate anterior. Si tu app necesita saber que "terminó la intro", modélalo como un trigger o una propiedad de tipo string en el archivo y obsérvalo.
¿Por qué no se actualiza mi binding?
La mayoría de los casos de "no pasa nada" se deben a alguno de estos motivos:
- La instancia no se conservó. Si la
ViewModelInstancees una variable local dentro de una función, desaparece en cuanto termina la configuración. Según la documentación de Rive: "Conserva referencias a las instancias que proporciones y que necesites leer o modificar después del binding. Las instancias vinculadas no se pueden recuperar a través de la máquina de estados." - Vinculaste una instancia y modificaste otra. Crear una segunda instancia con
createViewModelInstancete da una copia sin vincular. Asignarle valores no cambia nada en pantalla. - Usaste
Rive(file:)y luego buscaste la instancia. Los inicializadores de conveniencia vinculan la instancia predeterminada definida en el archivo, que no puedes recuperar; el getter obsoletorive.viewModelInstancedevuelvenilcon los inicializadores actuales. - Ruta o tipo incorrectos. Las rutas distinguen mayúsculas de minúsculas y las propiedades anidadas usan barras (
"settings/volume"). Un número del archivo debe leerse conNumberProperty, no conStringProperty. - Nombre de máquina de estados incorrecto.
createStateMachinelanzaArtboardError.invalidStateMachinesi el nombre no existe. No ignores ese error en silencio. - Llamadas fuera del main actor. Las APIs son
@MainActor. Marca tu modelo con@MainActor, como arriba, y el compilador lo hará cumplir.
Para inspeccionar un archivo desconocido, file.getViewModelNames(), file.getProperties(of:) y file.getDefaultViewModelInfo(for:) devuelven lo que el archivo contiene realmente.
¿Qué plataformas admite el nuevo runtime?
Ambos runtimes vienen en el mismo paquete, así que comparten sus versiones mínimas. El manifiesto Package.swift declara:
platforms: [.iOS("14.0"), .visionOS("1.0"), .tvOS("16.0"), .macOS("13.1"), .macCatalyst("14.0")]
No hay una entrada para watchOS, así que considera Apple Watch como no compatible, aunque el resumen de una página de migración haya sugerido lo contrario. Consulta animación de personajes en watchOS y visionOS para ver alternativas.
El tipo de vista es RiveUIView, un UIView basado en Metal (un NSView en macOS). RiveUIViewRepresentable lo envuelve para SwiftUI y agrega modificadores: .paused(_:), .frameRate(_:) (.default, .fps(30) o un .range) y .semantics(_:) para la semántica de VoiceOver definida en el editor, que solo admite el nuevo runtime.
¿Debo seguir usando RiveViewModel y los inputs de la máquina de estados?
Usa el runtime anterior cuando tengas un archivo que solo tiene inputs y no puedas modificarlo, cuando dependas de los eventos de Rive o cuando apuntes a una configuración de despliegue que ya publicaste y probaste. Sigue incluido, y setInput y triggerInput siguen funcionando ahí.
Para proyectos nuevos, Rive presenta el nuevo runtime como el camino a seguir. Una ruta de migración práctica:
- Convierte los inputs en view models en el editor, manteniendo los mismos nombres (
mood,success). - Reemplaza
RiveViewModel(fileName:stateMachineName:)por la cadena Worker → File → Artboard → StateMachine → Rive. - Reemplaza
viewModel.view()porRiveUIViewRepresentable(rive:). - Reemplaza
setInput("mood", value:)porsetValue(of: NumberProperty(path: "mood"), to:)ytriggerInputporfire(trigger:). - Reemplaza
pause()por.paused(true)ysetPreferredFramesPerSecondpor.frameRate(.fps(30)).
Las pantallas de UIKit siguen la misma correspondencia con RiveUIView; consulta Rive en UIKit.
Vincula un view model de Rive en SwiftUI
- Agrega el paquete. Agrega
https://github.com/rive-app/rive-ioscon Swift Package Manager (6.24.0 o posterior) e incluye tu .riv en el target de la app. - Carga el archivo. En un modelo
@MainActor, creatry await Worker()yFile(source: .local("character", Bundle.main), worker:). - Crea y conserva una instancia. Llama a
file.createViewModelInstance(.viewModelDefault(from: .artboardDefault(artboard)))y guarda el resultado en una propiedad. - Vincúlala. Crea la máquina de estados con
artboard.createStateMachine("CharacterViewModelMachine", binding: instance)y luegoRive(file:artboard:stateMachine:). - Renderiza. Coloca
RiveUIViewRepresentable(rive: model.rive)con un frame y cárgalo desde.task. - Contrólalo. Llama a
setValue(of: NumberProperty(path: "mood"), to: Float(5))para los loops y afire(trigger: TriggerProperty(path: "success"))para las reacciones puntuales.
Preguntas frecuentes
¿El nuevo runtime de Rive para Apple admite setInput?
No. Los inputs de la máquina de estados solo existen en el runtime anterior, y Rive indica que no tiene planes de recuperarlos. Convierte los inputs en propiedades de view model en el editor, conserva los mismos nombres y usa en su lugar NumberProperty, BoolProperty y TriggerProperty sobre una instancia de view model vinculada.
¿Por qué setValue de mi NumberProperty no compila con un Int?
El tipo de valor de NumberProperty es Float. Si pasas una variable Int, falla la verificación de tipos, así que conviértela: instance.setValue(of: NumberProperty(path: "mood"), to: Float(mood)). Un literal como 5 funciona porque Swift lo infiere como Float. Leer el valor con value(of:) o valueStream(of:) también devuelve un Float, así que conviértelo con Int(value) cuando lo asignes a un enum.
¿Todavía puedo usar Rive(file:artboard:stateMachine:dataBind:)?
Compila, pero está obsoleto, y el código fuente indica que se eliminará en una versión futura. Crea y conserva tú mismo la instancia del view model, vincúlala con artboard.createStateMachine(name, binding: instance) y luego usa el inicializador de Rive sin dataBind. Así también obtienes la referencia a la instancia que necesitas para setValue y fire.
¿Puedo usar un mismo Worker para varios archivos de Rive?
Sí. Los archivos creados con el mismo Worker comparten su hilo en segundo plano y los assets out-of-band registrados en él. Rive sugiere usar workers separados cuando varios gráficos pesados necesitan cada uno su propio hilo. En cualquier caso, mantén vivo el worker mientras sus archivos estén en uso.
Respuestas rápidas de esta guía
Fuentes
- Apple runtime (new and legacy), Rive
- Apple runtime: Data binding, Rive
- Migrating from the legacy Rive Apple runtime, Rive
- Artboard.swift (createStateMachine), Rive (GitHub)
- ViewModelInstance.swift, Rive (GitHub)
- Rive.swift, Rive (GitHub)
- RiveUIViewRepresentable.swift, Rive (GitHub)
- rive-ios Package.swift, Rive (GitHub)