Rive no UIKit: RiveUIView e a RiveView legada
Por equipe Charmotion · Atualizado em

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) é umaUIViewbaseada em Metal; a propriedaderivedela pode ser definida depois que a view já está na tela.RiveView(legado) vem deRiveViewModel.createRiveView(); mantenha o view model como propriedade, senão a view para de responder.- No novo runtime, pause com
isPausede limite os frames comframeRate = .fps(30); o legado usapause()esetPreferredFramesPerSecond. - Os humores são uma
NumberPropertyem 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 com | RiveUIView(rive:) ou RiveUIView({ async closure }) | riveViewModel.createRiveView() |
| Controlador | Um objeto Rive mais a sua ViewModelInstance mantida | O RiveViewModel que a criou |
| Mudança de estado | Propriedades e triggers de Data Binding | setInput, triggerInput (e data binding legado) |
| Threads | Thread em segundo plano do Worker, API no main actor | Thread principal |
| Pausar | isPaused = true | viewModel.pause() |
| Taxa de frames | frameRate = .fps(30) | setPreferredFramesPerSecond(preferredFramesPerSecond: 30) |
| Eventos, animações lineares | Não suportado | Suportado |
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:
| Legado | Novo 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 estado | Observe 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)
- Adicione a view. Crie
RiveUIView(rive: nil), adicione-a como subview e defina o tamanho dela com Auto Layout. - Carregue em uma Task. Em
viewDidLoad, inicieTask { await loadCharacter() }para que a configuração assíncrona rode no main actor. - Faça o bind de uma instância de view model. Crie a instância com
file.createViewModelInstance, guarde-a e passe-a paraartboard.createStateMachine(_:binding:). - Atribua o objeto Rive. Defina
riveView.rive = try await Rive(file:artboard:stateMachine:). - Controle e pause. Chame
setValue(of:to:)efire(trigger:)na instância guardada; definaisPausedquando 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
- 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)