Rive in UIKit: RiveUIView and Legacy RiveView
By the Charmotion team · Updated

In UIKit, the new Rive runtime renders through RiveUIView: build a Rive object from a Worker, File, artboard and bound state machine, then assign it to riveView.rive and add the view with Auto Layout. The legacy runtime uses RiveViewModel(fileName:stateMachineName:) and createRiveView(), driven with setInput and triggerInput.
Key takeaways
RiveUIView(new runtime) is a Metal-backedUIView; itsriveproperty can be set after the view is on screen.RiveView(legacy) comes fromRiveViewModel.createRiveView(); keep the view model as a property or the view stops responding.- Pause with
isPausedand cap frames withframeRate = .fps(30)in the new runtime; legacy usespause()andsetPreferredFramesPerSecond. - Moods are a
NumberPropertyon a retained view model instance in the new runtime, and a number input in the legacy one.
How do I add a Rive animation to a UIViewController?
With the new runtime, create a RiveUIView up front with rive: nil, lay it out, and assign the Rive object when loading finishes. Setting rive again later swaps the graphic.
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)")
}
}
}
A Task created in a UIViewController method inherits the main actor, which is what Rive's @MainActor APIs require. If you do not need the instance (a purely decorative loop), RiveUIView({ … }) also accepts an async closure that returns a Rive; Rive's docs show it with try await Rive(file: file), which binds the file's default instance automatically.
What is the difference between RiveUIView and the legacy RiveView?
Both are UIView subclasses in the same RiveRuntime module, but they belong to different runtimes and cannot be mixed for one graphic.
RiveUIView (new) | RiveView (legacy) | |
|---|---|---|
| Created with | RiveUIView(rive:) or RiveUIView({ async closure }) | riveViewModel.createRiveView() |
| Controller | A Rive object plus your retained ViewModelInstance | The RiveViewModel that created it |
| Changing state | Data binding properties and triggers | setInput, triggerInput (and legacy data binding) |
| Threading | Worker background thread, API on main actor | Main thread |
| Pause | isPaused = true | viewModel.pause() |
| Frame rate | frameRate = .fps(30) | setPreferredFramesPerSecond(preferredFramesPerSecond: 30) |
| Events, linear animations | Not supported | Supported |
Pick by file: an input-only .riv needs the legacy view; a file with view models works with either, and Rive steers new work to RiveUIView.
How do I create a RiveView with RiveViewModel.createRiveView()?
In the legacy runtime, the view model owns the file and state machine, and createRiveView() returns a RiveView wired to it. Store the view model as a property: it is the object you send inputs to.
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 is the bundle resource name without .riv. The initializer also takes fit (default .contain), alignment, autoPlay and artboardName. A RiveViewModel drives one view; create one per visible copy of the character.
In a table or collection view header, create the view model with the cell or header object, not in cellForRowAt, so scrolling does not reload the file each time.
How do I drive moods with setInput on the legacy runtime?
RiveViewModel exposes typed overloads: setInput(_:value:) for Bool, Float and Double, and triggerInput(_:) for triggers. Wire them to your view controller's state changes:
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
}
}
Input names are case-sensitive and must match the state machine. Rive documents inputs as a legacy feature: they keep working in this runtime, but new files should expose view models instead. Charmotion files carry both, so the same .riv works here through CharacterStateMachine and in the new runtime through CharacterViewModelMachine; the generated {Name}View.swift and {Name}NewRuntimeView.swift files show each.
How do I set the mood from UIKit on the new runtime?
Keep the view model instance you bound during setup (the character property in the first example) and call its typed setters. They queue the change and return immediately; no play() call is needed.
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:) emits each time a trigger fires, including when the file fires it from a listener. Cancel the task when the controller goes away, for example by storing it and calling cancel() in viewDidDisappear.
How do I pause Rive or cap its frame rate in UIKit?
Pause off-screen or decorative animations to save battery, and when Reduce Motion is on.
// 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 notes that on versions earlier than iOS 15 a frame-rate range behaves like a fixed .fps value. A good default for a small mascot is to pause in viewDidDisappear and resume in viewWillAppear. To react to Reduce Motion changes while the screen is open, see Reduce Motion in SwiftUI and UIKit.
How do I migrate a UIKit screen from RiveViewModel to the new runtime?
Rive's migration guide maps each legacy call to a new one:
| Legacy | New 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) |
| State-change delegate | Observe a property with valueStream(of:) |
Migrate one screen at a time: the two runtimes coexist in the same app and package. First make sure the file has view models (the editor's Convert Inputs to ViewModels helps), then swap the view, then the calls. The SwiftUI side of the same move is covered in Rive data binding in SwiftUI.
Show a Rive character in a UIKit view controller (new runtime)
- Add the view. Create
RiveUIView(rive: nil), add it as a subview and constrain its size with Auto Layout. - Load in a Task. From
viewDidLoad, startTask { await loadCharacter() }so the async setup runs on the main actor. - Bind a view model instance. Create the instance with
file.createViewModelInstance, store it, and pass it toartboard.createStateMachine(_:binding:). - Assign the Rive object. Set
riveView.rive = try await Rive(file:artboard:stateMachine:). - Drive and pause. Call
setValue(of:to:)andfire(trigger:)on the stored instance; setisPausedwhen the screen is hidden.
Frequently asked questions
Why does my legacy RiveView stop responding to setInput?
Usually the RiveViewModel was a local variable and was released after viewDidLoad, or a second view model was created for the same view. Store the view model as a property of the view controller, create the view from it once, and send every input to that same object.
Can I use RiveUIView in a UITableViewCell?
Yes, but avoid loading the file in cellForRowAt. Load the File once, keep it in a shared model, and create a Rive object per visible cell, or show a static image in the list and a live RiveUIView only in the detail screen. Pause views that scroll off screen.
Does RiveUIView work on macOS and visionOS?
RiveUIView is an NSView on macOS and a UIView elsewhere, and the package declares macOS 13.1, Mac Catalyst 14, tvOS 16 and visionOS 1 alongside iOS 14. There is no watchOS platform in the package manifest, so Apple Watch apps need another approach.
Quick answers from this guide
Sources
- 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)