Pular para o conteúdo
charmotion

Data Binding do Rive em SwiftUI (novo runtime da Apple)

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia de iOS e Swift: Rive Data Binding no SwiftUI (novo runtime da Apple), com personagens de demonstração do Charmotion

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 async e 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 mood Int com Float(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 Rive exige try 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, TriggerProperty e 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:

ObjetoCriado comTrabalho
Workertry await Worker()Controla a thread em segundo plano e os assets compartilhados carregados à parte (imagens, fontes, áudio).
Filetry await File(source:worker:)O .riv já analisado. As fontes podem ser .local(name, bundle), .url(URL) ou .data(Data).
Artboardtry await file.createArtboard()A tela a ser desenhada; passe um nome para usar um artboard que não seja o padrão.
StateMachinetry await artboard.createStateMachine(name, binding: instance)Executa a lógica e lê a instância do view model vinculada.
Rivetry 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.

moodLoopmoodLoop
0idle5loading
1happy6listening
2wave7talking
3thinking8sleeping
4sad9attention

Triggers: success, error, yes, no, enter, exit.

Dez estados em loop de um personagem, cada um selecionado por um número de mood de 0 a 9
Os dez estados em loop em um arquivo Rive do Charmotion, selecionados com o input numérico mood.

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:

  1. A instância não foi retida. Se o ViewModelInstance for 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."
  2. Você vinculou uma instância e alterou outra. Criar uma segunda instância com createViewModelInstance gera uma cópia não vinculada. Definir valores nela não muda nada na tela.
  3. 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 obsoleto rive.viewModelInstance retorna nil com os inicializadores atuais.
  4. 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 com NumberProperty, não com StringProperty.
  5. Nome de state machine errado. createStateMachine lança ArtboardError.invalidStateMachine se o nome não existir. Não engula esse erro silenciosamente.
  6. 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:

  1. Converta os inputs em view models no editor, mantendo os mesmos nomes (mood, success).
  2. Substitua RiveViewModel(fileName:stateMachineName:) pela cadeia Worker → File → Artboard → StateMachine → Rive.
  3. Substitua viewModel.view() por RiveUIViewRepresentable(rive:).
  4. Substitua setInput("mood", value:) por setValue(of: NumberProperty(path: "mood"), to:) e triggerInput por fire(trigger:).
  5. Substitua pause() por .paused(true) e setPreferredFramesPerSecond por .frameRate(.fps(30)).

Telas em UIKit seguem o mesmo mapeamento com RiveUIView; veja Rive no UIKit.

Vincular um view model do Rive no SwiftUI

  1. Adicione o pacote. Adicione https://github.com/rive-app/rive-ios com o Swift Package Manager (6.24.0 ou posterior) e inclua seu .riv no target do app.
  2. Carregue o arquivo. Em um model @MainActor, crie try await Worker() e File(source: .local("character", Bundle.main), worker:).
  3. Crie e retenha uma instância. Chame file.createViewModelInstance(.viewModelDefault(from: .artboardDefault(artboard))) e guarde o resultado em uma propriedade.
  4. Faça o binding. Crie a state machine com artboard.createStateMachine("CharacterViewModelMachine", binding: instance) e depois Rive(file:artboard:stateMachine:).
  5. Renderize. Posicione RiveUIViewRepresentable(rive: model.rive) com um frame e carregue-o a partir de .task.
  6. Controle a animação. Chame setValue(of: NumberProperty(path: "mood"), to: Float(5)) para os loops e fire(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

Guias relacionados