Pular para o conteúdo
charmotion

Rive no UIKit: RiveUIView e a RiveView legada

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia de iOS e Swift: Rive no UIKit: RiveUIView e a RiveView legada, com personagens de demonstração do Charmotion

No UIKit, o novo runtime do Rive renderiza por meio de RiveUIView: crie um objeto Rive a partir de um Worker, um File, um artboard e uma state machine com bind, depois atribua-o a riveView.rive e adicione a view com Auto Layout. O runtime legado usa RiveViewModel(fileName:stateMachineName:) e createRiveView(), controlados com setInput e triggerInput.

Principais pontos

  • RiveUIView (novo runtime) é uma UIView baseada em Metal; a propriedade rive dela pode ser definida depois que a view já está na tela.
  • RiveView (legado) vem de RiveViewModel.createRiveView(); mantenha o view model como propriedade, senão a view para de responder.
  • No novo runtime, pause com isPaused e limite os frames com frameRate = .fps(30); o legado usa pause() e setPreferredFramesPerSecond.
  • Os humores são uma NumberProperty em uma instância de view model mantida no novo runtime, e um input numérico no legado.

Como adicionar uma animação Rive a um UIViewController?

Com o novo runtime, crie uma RiveUIView logo de início com rive: nil, posicione-a no layout e atribua o objeto Rive quando o carregamento terminar. Definir rive de novo depois troca o 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)")
        }
    }
}

Uma Task criada em um método de UIViewController herda o main actor, que é o que as APIs @MainActor do Rive exigem. Se você não precisa da instância (um loop puramente decorativo), RiveUIView({ … }) também aceita uma closure assíncrona que retorna um Rive; a documentação do Rive mostra isso com try await Rive(file: file), que faz o bind da instância padrão do arquivo automaticamente.

Qual é a diferença entre RiveUIView e a RiveView legada?

Ambas são subclasses de UIView no mesmo módulo RiveRuntime, mas pertencem a runtimes diferentes e não podem ser misturadas em um mesmo gráfico.

RiveUIView (novo)RiveView (legado)
Criado comRiveUIView(rive:) ou RiveUIView({ async closure })riveViewModel.createRiveView()
ControladorUm objeto Rive mais a sua ViewModelInstance mantidaO RiveViewModel que a criou
Mudança de estadoPropriedades e triggers de Data BindingsetInput, triggerInput (e data binding legado)
ThreadsThread em segundo plano do Worker, API no main actorThread principal
PausarisPaused = trueviewModel.pause()
Taxa de framesframeRate = .fps(30)setPreferredFramesPerSecond(preferredFramesPerSecond: 30)
Eventos, animações linearesNão suportadoSuportado

Escolha pelo arquivo: um .riv só com inputs precisa da view legada; um arquivo com view models funciona com qualquer uma, e o Rive recomenda RiveUIView para projetos novos.

Como criar uma RiveView com RiveViewModel.createRiveView()?

No runtime legado, o view model é dono do arquivo e da state machine, e createRiveView() retorna uma RiveView conectada a ele. Guarde o view model como propriedade: é para esse objeto que você envia os 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 é o nome do recurso no bundle sem .riv. O inicializador também aceita fit (padrão .contain), alignment, autoPlay e artboardName. Um RiveViewModel controla uma única view; crie um para cada cópia visível do personagem.

Em um header de table view ou collection view, crie o view model junto com o objeto da célula ou do header, não em cellForRowAt, para que a rolagem não recarregue o arquivo toda vez.

Como controlar os humores com setInput no runtime legado?

RiveViewModel oferece sobrecargas tipadas: setInput(_:value:) para Bool, Float e Double, e triggerInput(_:) para triggers. Conecte-as às mudanças de estado do seu 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
    }
}

Os nomes dos inputs diferenciam maiúsculas de minúsculas e precisam corresponder à state machine. O Rive documenta os inputs como um recurso legado: eles continuam funcionando neste runtime, mas arquivos novos devem expor view models. Os arquivos do Charmotion trazem os dois, então o mesmo .riv funciona aqui via CharacterStateMachine e no novo runtime via CharacterViewModelMachine; os arquivos gerados {Name}View.swift e {Name}NewRuntimeView.swift mostram cada caso.

Como definir o humor pelo UIKit no novo runtime?

Mantenha a instância do view model que você fez bind na configuração (a propriedade character no primeiro exemplo) e chame os setters tipados dela. Eles enfileiram a mudança e retornam na hora; não é preciso chamar 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 um valor sempre que um trigger é disparado, inclusive quando o próprio arquivo o dispara a partir de um listener. Cancele a task quando o controller for descartado, por exemplo guardando-a e chamando cancel() em viewDidDisappear.

Como pausar o Rive ou limitar a taxa de frames no UIKit?

Pause animações fora da tela ou decorativas para economizar bateria e também quando o Reduzir Movimento estiver ativado.

// 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)

O Rive observa que, em versões anteriores ao iOS 15, um intervalo de taxa de frames se comporta como um valor fixo de .fps. Um bom padrão para um mascote pequeno é pausar em viewDidDisappear e retomar em viewWillAppear. Para reagir a mudanças em Reduzir Movimento com a tela aberta, veja Reduzir Movimento no SwiftUI e no UIKit.

Como migrar uma tela UIKit do RiveViewModel para o novo runtime?

O guia de migração do Rive mostra a chamada nova equivalente a cada chamada legada:

LegadoNovo 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 mudança de estadoObserve uma propriedade com valueStream(of:)

Migre uma tela por vez: os dois runtimes convivem no mesmo app e no mesmo pacote. Primeiro confira se o arquivo tem view models (a opção Convert Inputs to ViewModels do editor ajuda), depois troque a view e, por fim, as chamadas. O lado SwiftUI dessa mesma migração está em Rive Data Binding no SwiftUI.

Mostrar um personagem Rive em um view controller UIKit (novo runtime)

  1. Adicione a view. Crie RiveUIView(rive: nil), adicione-a como subview e defina o tamanho dela com Auto Layout.
  2. Carregue em uma Task. Em viewDidLoad, inicie Task { await loadCharacter() } para que a configuração assíncrona rode no main actor.
  3. Faça o bind de uma instância de view model. Crie a instância com file.createViewModelInstance, guarde-a e passe-a para artboard.createStateMachine(_:binding:).
  4. Atribua o objeto Rive. Defina riveView.rive = try await Rive(file:artboard:stateMachine:).
  5. Controle e pause. Chame setValue(of:to:) e fire(trigger:) na instância guardada; defina isPaused quando a tela estiver oculta.

Perguntas frequentes

Por que minha RiveView legada para de responder ao setInput?

Geralmente o RiveViewModel era uma variável local e foi liberado depois do viewDidLoad, ou um segundo view model foi criado para a mesma view. Guarde o view model como propriedade do view controller, crie a view a partir dele uma única vez e envie todos os inputs para esse mesmo objeto.

Posso usar RiveUIView em uma UITableViewCell?

Sim, mas evite carregar o arquivo em cellForRowAt. Carregue o File uma vez, mantenha-o em um modelo compartilhado e crie um objeto Rive por célula visível, ou mostre uma imagem estática na lista e uma RiveUIView ao vivo só na tela de detalhes. Pause as views que saírem da tela com a rolagem.

A RiveUIView funciona no macOS e no visionOS?

A RiveUIView é uma NSView no macOS e uma UIView nas demais plataformas, e o pacote declara macOS 13.1, Mac Catalyst 14, tvOS 16 e visionOS 1, além do iOS 14. Não há plataforma watchOS no manifesto do pacote, então apps para Apple Watch precisam de outra abordagem.

Respostas rápidas deste guia

Fontes

Guias relacionados