Data Binding do Rive em SwiftUI (novo runtime da Apple)
Por equipe Charmotion · Atualizado em

Para usar Data Binding do Rive no SwiftUI, crie um Worker e um File, gere uma instância de view model com file.createViewModelInstance, vincule-a com artboard.createStateMachine(name, binding: instance) e renderize Rive(file:artboard:stateMachine:) por meio de RiveUIViewRepresentable(rive:). Guarde a instância e chame setValue(of:to:) e fire(trigger:) nela.
Principais pontos
- O novo runtime para Apple chegou no rive-ios 6.24.0 e usa Swift Concurrency; as chamadas de configuração são
asynce todas as chamadas rodam no main actor. - Ele não tem inputs de state machine. Números, booleanos e triggers ficam em um view model que você vincula explicitamente.
- Guarde você mesmo a
ViewModelInstance: a state machine não tem getter para instâncias vinculadas. NumberProperty.ValueéFloat, então converta um moodIntcomFloat(mood).- Não use o inicializador obsoleto
Rive(file:…dataBind:)em código novo.
O que mudou no novo runtime do Rive para Apple?
O pacote do Rive para Apple (RiveRuntime, de rive-app/rive-ios) agora contém duas APIs. O runtime legado é construído em torno do RiveViewModel e, nas palavras do Rive, "atualmente roda em uma única thread, a thread principal". O novo runtime, lançado na versão 6.24.0, é "uma API pensada primeiro para Swift que aproveita o Swift Concurrency, com suporte aprimorado a multithreading". No momento em que este texto foi escrito (outubro de 2026), a versão mais recente é a 6.28.1; a 6.28.0 adicionou view models globais e data binding de blobs.
Na prática, três diferenças importam:
- Tudo é assíncrono. Criar um worker, um file, um artboard, uma state machine e o objeto
Riveexigetry await. O Rive ainda exige que as chamadas sejam feitas no main actor; o trabalho pesado acontece na thread em segundo plano do worker. - Sem inputs, eventos ou animações lineares. O guia de migração lista inputs de state machine e listeners de eventos como exclusivos do runtime legado, sem "planos atuais de reintroduzi-los". Animações lineares precisam ser colocadas dentro de uma state machine.
- O Data Binding é o contrato. Seu código se comunica com uma instância de view model por meio de propriedades tipadas (
NumberProperty,TriggerPropertye assim por diante) acessadas por caminho.
Se o seu .riv expõe apenas inputs, continue no runtime legado (veja Rive no SwiftUI) ou use antes a opção Menu → Convert Inputs to ViewModels do editor.
Como Worker, File, Artboard e Rive se encaixam?
O novo runtime divide o que o RiveViewModel fazia em cinco objetos. Cada um é criado a partir do anterior:
| Objeto | Criado com | Trabalho |
|---|---|---|
Worker | try await Worker() | Controla a thread em segundo plano e os assets compartilhados carregados à parte (imagens, fontes, áudio). |
File | try await File(source:worker:) | O .riv já analisado. As fontes podem ser .local(name, bundle), .url(URL) ou .data(Data). |
Artboard | try await file.createArtboard() | A tela a ser desenhada; passe um nome para usar um artboard que não seja o padrão. |
StateMachine | try await artboard.createStateMachine(name, binding: instance) | Executa a lógica e lê a instância do view model vinculada. |
Rive | try await Rive(file:artboard:stateMachine:) | A configuração de exibição (fit, fundo) repassada à view. |
A documentação do Rive diz que um Worker "precisa permanecer ativo durante todo o uso do Rive" e que um File mantém uma referência forte ao seu worker. Um worker equivale, grosso modo, a uma thread em segundo plano. Para vários gráficos pesados, você pode dar a cada arquivo o próprio worker ou compartilhar um único worker para que os arquivos compartilhem os assets registrados.
Atenção ao inicializador: o código-fonte expõe Worker() e Worker(configuration:). Alguns trechos de migração mostram Worker.shared(), que é um helper do app no exemplo do Rive, e não faz parte do RiveRuntime.
Como criar uma instância de view model e vinculá-la a uma state machine?
Crie a instância a partir do arquivo e passe-a para createStateMachine(_:binding:). .viewModelDefault(from: .artboardDefault(artboard)) significa "a instância padrão do view model atribuído a este artboard". Outras fontes são .blank(from:) e .name(_:from:), para uma instância nomeada, e ViewModelSource.name("Character") seleciona um view model pelo nome.
Coloque a configuração em um model observável para que a instância sobreviva ao body da view:
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 recebe um Rive opcional, então a view pode aparecer antes de o carregamento terminar. No código-fonte, o inicializador tem rótulo (rive:), embora alguns trechos da documentação o omitam. Se preferir manter o carregamento dentro da view, AsyncRiveUIViewRepresentable { … } recebe uma closure assíncrona que retorna um Rive; nesse caso, você precisa de outra forma de manter a instância.
Sempre que possível, defina os valores iniciais antes de a view ser apresentada. O guia de migração do Rive observa que isso evita um frame exibindo os valores padrão do arquivo.
Como definir o número do mood e disparar triggers no SwiftUI?
Definir uma propriedade enfileira a alteração e retorna imediatamente; ao contrário do runtime legado, você não chama play() depois. Triggers não têm valor, então você os dispara:
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 a state machine controla as transições, seu código só informa o que o app está fazendo. Um arquivo bem construído deixa a reação única do trigger terminar e depois volta para o loop que mood estiver selecionando no momento.
Os arquivos do Charmotion usam exatamente este contrato: um view model Character em CharacterViewModelMachine, com estes valores. Cada conjunto dos planos pagos também traz um {Name}NewRuntimeView.swift pronto para este runtime e um {Name}View.swift para o RiveViewModel clássico.
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.

Como observar uma alteração de propriedade vinda do Rive?
O Data Binding é bidirecional: a state machine pode alterar uma propriedade, por exemplo quando o usuário toca em uma área de toque do artboard. Propriedades de string, número, booleano, cor e enum expõem valueStream(of:); triggers expõem stream(of:), que emite Void a cada disparo do trigger. Para ler uma única vez, use 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
}
}
}
Inicie esses loops a partir de .task para que o SwiftUI os cancele quando a view desaparecer. Segundo a documentação do Rive, o erro lançado é um ViewModelInstanceError. Você não precisa do dirtyStream() obsoleto para forçar redesenhos; o controller redesenha sozinho após alterações nas instâncias vinculadas.
O novo runtime não tem um callback de nome de estado como o RiveStateDelegate legado. Se o seu app precisa saber que "a intro terminou", modele isso como um trigger ou uma propriedade de string no arquivo e observe-a.
Por que meu binding não está atualizando?
A maioria dos relatos de "nada acontece" se resume a um destes casos:
- A instância não foi retida. Se o
ViewModelInstancefor uma variável local em uma função, ele deixa de existir assim que a configuração retorna. Da documentação do Rive: "Mantenha referências a todas as instâncias fornecidas que você precisar ler ou modificar após o binding. Instâncias vinculadas não podem ser recuperadas pela state machine." - Você vinculou uma instância e alterou outra. Criar uma segunda instância com
createViewModelInstancegera uma cópia não vinculada. Definir valores nela não muda nada na tela. - Você usou
Rive(file:)e depois procurou a instância. Os inicializadores de conveniência vinculam a instância padrão criada no editor, que você não consegue recuperar; o getter obsoletorive.viewModelInstanceretornanilcom os inicializadores atuais. - Caminho ou tipo errado. Os caminhos diferenciam maiúsculas de minúsculas, e propriedades aninhadas usam barras (
"settings/volume"). Um número no arquivo precisa ser lido comNumberProperty, não comStringProperty. - Nome de state machine errado.
createStateMachinelançaArtboardError.invalidStateMachinese o nome não existir. Não engula esse erro silenciosamente. - Chamadas fora do main actor. As APIs são
@MainActor. Marque seu model com@MainActor, como acima, e o compilador garante isso.
Para inspecionar um arquivo desconhecido, file.getViewModelNames(), file.getProperties(of:) e file.getDefaultViewModelInfo(for:) retornam o que o arquivo realmente contém.
Quais plataformas o novo runtime suporta?
Os dois runtimes são distribuídos no mesmo pacote, então compartilham as mesmas versões mínimas. O manifesto Package.swift declara:
platforms: [.iOS("14.0"), .visionOS("1.0"), .tvOS("16.0"), .macOS("13.1"), .macCatalyst("14.0")]
Não há entrada para watchOS, então considere o Apple Watch como não suportado, mesmo que o resumo de uma página de migração já tenha sugerido o contrário. Veja animação de personagem no watchOS e no visionOS para alternativas.
O tipo de view é RiveUIView, uma UIView baseada em Metal (uma NSView no macOS). RiveUIViewRepresentable a encapsula para o SwiftUI e adiciona modificadores: .paused(_:), .frameRate(_:) (.default, .fps(30) ou um .range) e .semantics(_:), para a semântica de VoiceOver definida no editor, recurso que só o novo runtime suporta.
Ainda devo usar RiveViewModel e inputs de state machine?
Use o runtime legado quando tiver um arquivo que só tem inputs e que você não pode alterar, quando depender de eventos do Rive ou quando o alvo for uma configuração de deploy que você já publicou e testou. Ele continua sendo distribuído, e setInput e triggerInput continuam funcionando nele.
Para projetos novos, o Rive apresenta o novo runtime como o caminho a seguir. Um roteiro prático de migração:
- Converta os inputs em view models no editor, mantendo os mesmos nomes (
mood,success). - Substitua
RiveViewModel(fileName:stateMachineName:)pela cadeia Worker → File → Artboard → StateMachine → Rive. - Substitua
viewModel.view()porRiveUIViewRepresentable(rive:). - Substitua
setInput("mood", value:)porsetValue(of: NumberProperty(path: "mood"), to:)etriggerInputporfire(trigger:). - Substitua
pause()por.paused(true)esetPreferredFramesPerSecondpor.frameRate(.fps(30)).
Telas em UIKit seguem o mesmo mapeamento com RiveUIView; veja Rive no UIKit.
Vincular um view model do Rive no SwiftUI
- Adicione o pacote. Adicione
https://github.com/rive-app/rive-ioscom o Swift Package Manager (6.24.0 ou posterior) e inclua seu .riv no target do app. - Carregue o arquivo. Em um model
@MainActor, crietry await Worker()eFile(source: .local("character", Bundle.main), worker:). - Crie e retenha uma instância. Chame
file.createViewModelInstance(.viewModelDefault(from: .artboardDefault(artboard)))e guarde o resultado em uma propriedade. - Faça o binding. Crie a state machine com
artboard.createStateMachine("CharacterViewModelMachine", binding: instance)e depoisRive(file:artboard:stateMachine:). - Renderize. Posicione
RiveUIViewRepresentable(rive: model.rive)com um frame e carregue-o a partir de.task. - Controle a animação. Chame
setValue(of: NumberProperty(path: "mood"), to: Float(5))para os loops efire(trigger: TriggerProperty(path: "success"))para as reações únicas.
Perguntas frequentes
O novo runtime Apple do Rive suporta setInput?
Não. Inputs de state machine existem apenas no runtime legado, e o Rive diz que não há planos de trazê-los de volta. Converta os inputs em propriedades de view model no editor, mantenha os mesmos nomes e use NumberProperty, BoolProperty e TriggerProperty em uma instância de view model vinculada.
Por que o setValue da minha NumberProperty não compila com um Int?
O tipo de valor de NumberProperty é Float. Passar uma variável Int falha na verificação de tipos, então converta: instance.setValue(of: NumberProperty(path: "mood"), to: Float(mood)). Um literal como 5 funciona porque o Swift o infere como Float. Ler o valor de volta com value(of:) ou valueStream(of:) também retorna um Float, então converta com Int(value) ao mapeá-lo para um enum.
Ainda posso usar Rive(file:artboard:stateMachine:dataBind:)?
Ele compila, mas está obsoleto, e o código-fonte diz que será removido em uma versão futura. Crie e mantenha você mesmo a instância do view model, faça o bind com artboard.createStateMachine(name, binding: instance) e depois use o inicializador Rive sem dataBind. Assim você também fica com a referência da instância necessária para setValue e fire.
Posso usar um Worker para vários arquivos Rive?
Sim. Arquivos criados com o mesmo Worker compartilham a thread em segundo plano dele e todos os assets out-of-band registrados nele. O Rive sugere workers separados quando vários gráficos pesados precisam de uma thread própria cada. De qualquer forma, mantenha o worker ativo enquanto os arquivos dele estiverem em uso.
Respostas rápidas deste guia
Fontes
- 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)