Rive en UIKit: RiveUIView y RiveView (legacy)
Por el equipo de Charmotion · Actualizado el

En UIKit, el nuevo runtime de Rive renderiza a través de RiveUIView: crea un objeto Rive a partir de un Worker, un File, un artboard y una máquina de estados (state machine) vinculada, asígnalo a riveView.rive y agrega la vista con Auto Layout. El runtime heredado (legacy) usa RiveViewModel(fileName:stateMachineName:) y createRiveView(), y se controla con setInput y triggerInput.
Puntos clave
RiveUIView(nuevo runtime) es unaUIViewbasada en Metal; su propiedadrivese puede asignar cuando la vista ya está en pantalla.RiveView(heredado) se obtiene deRiveViewModel.createRiveView(); guarda el view model como propiedad o la vista dejará de responder.- En el nuevo runtime, pausa con
isPausedy limita los cuadros por segundo conframeRate = .fps(30); el heredado usapause()ysetPreferredFramesPerSecond. - Los estados de ánimo son una
NumberPropertyen una instancia de view model conservada en el nuevo runtime, y un input numérico en el heredado.
¿Cómo agrego una animación de Rive a un UIViewController?
Con el nuevo runtime, crea desde el principio una RiveUIView con rive: nil, colócala en el layout y asigna el objeto Rive cuando termine la carga. Si vuelves a asignar rive más adelante, se reemplaza el gráfico.
import UIKit
import RiveRuntime
final class WelcomeViewController: UIViewController {
private let riveView = RiveUIView(rive: nil)
private var character: ViewModelInstance?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
riveView.translatesAutoresizingMaskIntoConstraints = false
riveView.isAccessibilityElement = true
riveView.accessibilityLabel = "Animated mascot"
view.addSubview(riveView)
NSLayoutConstraint.activate([
riveView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
riveView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
riveView.widthAnchor.constraint(equalToConstant: 240),
riveView.heightAnchor.constraint(equalTo: riveView.widthAnchor),
])
Task { await loadCharacter() }
}
private func loadCharacter() async {
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 machine = try await artboard.createStateMachine(
"CharacterViewModelMachine",
binding: instance
)
character = instance
riveView.rive = try await Rive(file: file, artboard: artboard, stateMachine: machine)
} catch {
print("Rive failed to load: \(error)")
}
}
}
Una Task creada en un método de UIViewController hereda el main actor, que es lo que requieren las API @MainActor de Rive. Si no necesitas la instancia (un loop puramente decorativo), RiveUIView({ … }) también acepta un closure asíncrono que devuelve un Rive; la documentación de Rive lo muestra con try await Rive(file: file), que vincula automáticamente la instancia predeterminada del archivo.
¿Cuál es la diferencia entre RiveUIView y el RiveView heredado?
Ambos son subclases de UIView en el mismo módulo RiveRuntime, pero pertenecen a runtimes distintos y no se pueden combinar en un mismo gráfico.
RiveUIView (nuevo) | RiveView (heredado) | |
|---|---|---|
| Se crea con | RiveUIView(rive:) o RiveUIView({ async closure }) | riveViewModel.createRiveView() |
| Controlador | Un objeto Rive más tu ViewModelInstance conservada | El RiveViewModel que la creó |
| Cambio de estado | Propiedades y triggers de Data Binding | setInput, triggerInput (y el Data Binding heredado) |
| Hilos | Hilo en segundo plano del Worker, API en el main actor | Hilo principal |
| Pausa | isPaused = true | viewModel.pause() |
| Cuadros por segundo | frameRate = .fps(30) | setPreferredFramesPerSecond(preferredFramesPerSecond: 30) |
| Eventos, animaciones lineales | No compatible | Compatible |
Elige según el archivo: un .riv que solo tiene inputs necesita la vista heredada; un archivo con view models funciona con cualquiera de las dos, y Rive recomienda usar RiveUIView para proyectos nuevos.
¿Cómo creo un RiveView con RiveViewModel.createRiveView()?
En el runtime heredado, el view model es dueño del archivo y de la máquina de estados, y createRiveView() devuelve un RiveView conectado a él. Guarda el view model como propiedad: es el objeto al que envías los inputs.
import UIKit
import RiveRuntime
final class InboxEmptyViewController: UIViewController {
private let mascot = RiveViewModel(
fileName: "character",
stateMachineName: "CharacterStateMachine"
)
override func viewDidLoad() {
super.viewDidLoad()
let riveView = mascot.createRiveView()
riveView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(riveView)
NSLayoutConstraint.activate([
riveView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
riveView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 48),
riveView.widthAnchor.constraint(equalToConstant: 200),
riveView.heightAnchor.constraint(equalToConstant: 200),
])
mascot.setInput("mood", value: Float(8)) // sleeping: calm empty state
}
}
fileName es el nombre del recurso en el bundle sin .riv. El inicializador también acepta fit (por defecto .contain), alignment, autoPlay y artboardName. Un RiveViewModel controla una sola vista; crea uno por cada copia visible del personaje.
En el header de una table view o collection view, crea el view model junto con el objeto de la celda o del header, no en cellForRowAt, para que el scroll no vuelva a cargar el archivo cada vez.
¿Cómo controlo los estados de ánimo con setInput en el runtime heredado?
RiveViewModel ofrece sobrecargas tipadas: setInput(_:value:) para Bool, Float y Double, y triggerInput(_:) para triggers. Conéctalas a los cambios de estado de tu view controller:
extension InboxEmptyViewController {
func refreshStarted() {
mascot.setInput("mood", value: Float(5)) // loading
}
func refreshFinished(newItems: Int) {
if newItems > 0 {
mascot.triggerInput("success")
mascot.setInput("mood", value: Float(1)) // happy
} else {
mascot.setInput("mood", value: Float(8)) // sleeping
}
}
func refreshFailed() {
mascot.triggerInput("error")
mascot.setInput("mood", value: Float(4)) // sad
}
}
Los nombres de los inputs distinguen mayúsculas y minúsculas y deben coincidir con la máquina de estados. Rive documenta los inputs como una función heredada: siguen funcionando en este runtime, pero los archivos nuevos deberían exponer view models. Los archivos de Charmotion incluyen ambos, así que el mismo .riv funciona aquí mediante CharacterStateMachine y en el nuevo runtime mediante CharacterViewModelMachine; los archivos generados {Name}View.swift y {Name}NewRuntimeView.swift muestran cada caso.
¿Cómo cambio el estado de ánimo desde UIKit en el nuevo runtime?
Conserva la instancia de view model que vinculaste durante la configuración (la propiedad character del primer ejemplo) y llama a sus setters tipados. Ponen el cambio en cola y devuelven el control de inmediato; no hace falta llamar a play().
extension WelcomeViewController {
private static let mood = NumberProperty(path: "mood")
func showListening() {
character?.setValue(of: Self.mood, to: Float(6))
}
func celebrate() {
character?.fire(trigger: TriggerProperty(path: "success"))
}
func observeTaps() {
guard let character else { return }
Task {
for try await _ in character.stream(of: TriggerProperty(path: "yes")) {
print("The user tapped yes inside the artboard")
}
}
}
}
stream(of:) emite un valor cada vez que se dispara un trigger, incluso cuando el propio archivo lo dispara desde un listener. Cancela la tarea cuando el controlador desaparezca, por ejemplo guardándola y llamando a cancel() en viewDidDisappear.
¿Cómo pauso Rive o limito sus cuadros por segundo en UIKit?
Pausa las animaciones fuera de pantalla o decorativas para ahorrar batería, y también cuando Reducir movimiento esté activado.
// New runtime
riveView.isPaused = true
riveView.frameRate = .fps(30)
riveView.frameRate = .range(minimum: 30, maximum: 60, preferred: nil)
riveView.frameRate = .default
// Legacy runtime
mascot.pause()
mascot.play()
mascot.setPreferredFramesPerSecond(preferredFramesPerSecond: 30)
Rive indica que, en versiones anteriores a iOS 15, un rango de cuadros por segundo se comporta como un valor .fps fijo. Una buena configuración por defecto para una mascota pequeña es pausar en viewDidDisappear y reanudar en viewWillAppear. Para reaccionar a los cambios de Reducir movimiento mientras la pantalla está abierta, consulta Reducir movimiento en SwiftUI y UIKit.
¿Cómo migro una pantalla de UIKit de RiveViewModel al nuevo runtime?
La guía de migración de Rive asigna cada llamada heredada a una nueva:
| Heredado | Nuevo runtime |
|---|---|
RiveViewModel(fileName:stateMachineName:) | Worker() → File(source:worker:) → createArtboard() → createStateMachine(_:binding:) → Rive(file:artboard:stateMachine:) |
createRiveView() | RiveUIView(rive:) |
viewModel.view() (SwiftUI) | RiveUIViewRepresentable(rive:) |
setInput("mood", value:) | setValue(of: NumberProperty(path: "mood"), to:) |
triggerInput("success") | fire(trigger: TriggerProperty(path: "success")) |
pause() / play() | isPaused = true / false |
setPreferredFramesPerSecond(preferredFramesPerSecond: 30) | frameRate = .fps(30) |
| Delegate de cambio de estado | Observa una propiedad con valueStream(of:) |
Migra una pantalla a la vez: los dos runtimes conviven en la misma app y el mismo paquete. Primero asegúrate de que el archivo tenga view models (la opción Convert Inputs to ViewModels del editor ayuda), luego reemplaza la vista y después las llamadas. La parte de SwiftUI de esta misma migración se explica en Data Binding de Rive en SwiftUI.
Muestra un personaje de Rive en un view controller de UIKit (nuevo runtime)
- Agrega la vista. Crea
RiveUIView(rive: nil), agrégala como subvista y define su tamaño con restricciones de Auto Layout. - Carga en una Task. Desde
viewDidLoad, iniciaTask { await loadCharacter() }para que la configuración asíncrona se ejecute en el main actor. - Vincula una instancia de view model. Crea la instancia con
file.createViewModelInstance, guárdala y pásala aartboard.createStateMachine(_:binding:). - Asigna el objeto Rive. Asigna
riveView.rive = try await Rive(file:artboard:stateMachine:). - Controla y pausa. Llama a
setValue(of:to:)yfire(trigger:)en la instancia guardada; asignaisPausedcuando la pantalla esté oculta.
Preguntas frecuentes
¿Por qué mi RiveView heredado deja de responder a setInput?
Normalmente, el RiveViewModel era una variable local y se liberó después de viewDidLoad, o se creó un segundo view model para la misma vista. Guarda el view model como propiedad del view controller, crea la vista a partir de él una sola vez y envía todos los inputs a ese mismo objeto.
¿Puedo usar RiveUIView en una UITableViewCell?
Sí, pero evita cargar el archivo en cellForRowAt. Carga el File una vez, guárdalo en un modelo compartido y crea un objeto Rive por cada celda visible, o muestra una imagen estática en la lista y una RiveUIView en vivo solo en la pantalla de detalle. Pausa las vistas que salen de la pantalla con el scroll.
¿RiveUIView funciona en macOS y visionOS?
RiveUIView es una NSView en macOS y una UIView en las demás plataformas, y el paquete declara macOS 13.1, Mac Catalyst 14, tvOS 16 y visionOS 1, además de iOS 14. El manifiesto del paquete no incluye la plataforma watchOS, así que las apps para Apple Watch necesitan otro enfoque.
Respuestas rápidas de esta guía
Fuentes
- Apple runtime (new and legacy), Rive
- Migrating from the legacy Rive Apple runtime, Rive
- Apple runtime: Inputs (legacy), Rive
- RiveUIView.swift, Rive (GitHub)
- RiveViewModel.swift (legacy runtime), Rive (GitHub)
- Artboard.swift (createStateMachine), Rive (GitHub)
- rive-ios Package.swift, Rive (GitHub)