Ir al contenido
charmotion

Rive en UIKit: RiveUIView y RiveView (legacy)

Por el equipo de Charmotion · Actualizado el

Ilustración de portada de la guía de iOS y Swift: Rive en UIKit: RiveUIView y Legacy RiveView, con personajes de demostración de Charmotion

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 una UIView basada en Metal; su propiedad rive se puede asignar cuando la vista ya está en pantalla.
  • RiveView (heredado) se obtiene de RiveViewModel.createRiveView(); guarda el view model como propiedad o la vista dejará de responder.
  • En el nuevo runtime, pausa con isPaused y limita los cuadros por segundo con frameRate = .fps(30); el heredado usa pause() y setPreferredFramesPerSecond.
  • Los estados de ánimo son una NumberProperty en 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 conRiveUIView(rive:) o RiveUIView({ async closure })riveViewModel.createRiveView()
ControladorUn objeto Rive más tu ViewModelInstance conservadaEl RiveViewModel que la creó
Cambio de estadoPropiedades y triggers de Data BindingsetInput, triggerInput (y el Data Binding heredado)
HilosHilo en segundo plano del Worker, API en el main actorHilo principal
PausaisPaused = trueviewModel.pause()
Cuadros por segundoframeRate = .fps(30)setPreferredFramesPerSecond(preferredFramesPerSecond: 30)
Eventos, animaciones linealesNo compatibleCompatible

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:

HeredadoNuevo 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 estadoObserva 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)

  1. Agrega la vista. Crea RiveUIView(rive: nil), agrégala como subvista y define su tamaño con restricciones de Auto Layout.
  2. Carga en una Task. Desde viewDidLoad, inicia Task { await loadCharacter() } para que la configuración asíncrona se ejecute en el main actor.
  3. Vincula una instancia de view model. Crea la instancia con file.createViewModelInstance, guárdala y pásala a artboard.createStateMachine(_:binding:).
  4. Asigna el objeto Rive. Asigna riveView.rive = try await Rive(file:artboard:stateMachine:).
  5. Controla y pausa. Llama a setValue(of:to:) y fire(trigger:) en la instancia guardada; asigna isPaused cuando 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

Guías relacionadas