The new Rive Apple runtime is a near-complete rewrite of both the public API and the internal architecture. While conceptually most operations have an equivalent, the two APIs are incompatible. Any existing work in the legacy API that you would like to migrate must be rebuilt using the new API.This guide covers:
This guide is not meant to be exhaustive as it would be redundant with existing general documentation. Please refer to the relevant sections of the documentation for more details on specific topics.
The new Apple runtime is available in the same Swift package and CocoaPods pod as the legacy runtime.The new Apple runtime APIs are currently behind the @_spi(RiveExperimental) SPI:
Copy
@_spi(RiveExperimental) import RiveRuntime
Compared to the legacy runtime:
Copy
import RiveRuntime
This is a temporary transition mechanism while the new API remains experimental.
The legacy runtime is effectively main-thread driven through RiveViewModel and RiveView.The new runtime introduces Worker objects for background processing while still enforcing API calls on the main actor. In practice:
Worker owns the background processing context
File strongly references its Worker
Out-of-band assets registered on a worker can be shared across files created with that worker
For more information, see the Threading section for additional details.
For most apps, a shared worker is recommended:
Copy
actor WorkerProvider { static let shared = WorkerProvider() @MainActor private var cachedWorker: Worker? @MainActor func worker() async throws -> Worker { if let cachedWorker { return cachedWorker } let worker = try await Worker() cachedWorker = worker return worker }}
For more information, see artboards documentation for more details.
For more information, see state machines documentation for more details.
Copy
let file = try RiveFile(name: "my_rive_file")let model = RiveModel(riveFile: file)model.setArtboard("My Artboard")model.setStateMachine("My State Machine")
For more information, see the layout docs for all fit and alignment options.
Copy
let viewModel = RiveViewModel( fileName: "my_rive_file", fit: .contain, alignment: .center)// Update at runtimeviewModel.fit = .fitWidthviewModel.alignment = .topCenter
To use artboard layout sizing in the legacy runtime:
Copy
let viewModel = RiveViewModel(fileName: "my_rive_file")viewModel.fit = .layoutviewModel.layoutScaleFactor = RiveViewModel.layoutScaleFactorAutomatic // default behavior// Or explicitly set a scale factorviewModel.layoutScaleFactor = 2.0
Copy
let worker = try await WorkerProvider.shared.worker()let file = try await File(source: .local("my_rive_file", Bundle.main), worker: worker)var rive = try await Rive(file: file, fit: .contain(alignment: .center))// Update at runtimerive.fit = .fitWidth(alignment: .topCenter)
To use artboard layout sizing in the new runtime:
Copy
let worker = try await WorkerProvider.shared.worker()let file = try await File(source: .local("my_rive_file", Bundle.main), worker: worker)var rive = try await Rive(file: file, fit: .layout(scaleFactor: .automatic))// Or explicitly set a scale factorrive.fit = .layout(scaleFactor: .explicit(2.0))
When using layout fit, both runtimes support automatic and explicit scale factors.Use RiveViewModel.layoutScaleFactorAutomatic (default) or set an explicit numeric value on layoutScaleFactor.Use .layout(scaleFactor: .automatic) or .layout(scaleFactor: .explicit(...)) on Rive.fit.
Data-bound values are applied when the bound state machine or artboard advances.Best practice for migration: if you need initial values, set them before creating and presenting a view.This avoids showing default values for a frame before your app-provided values are applied.Set initial values before the view is shown:
Copy
let viewModel = RiveViewModel(...)viewModel.riveModel?.enableAutoBind { boundInstance in boundInstance.stringProperty(fromPath: "path/to/string")?.value = "Initial Value"}// Present the view after initial values are setlet riveView = viewModel.createRiveView()
In the new runtime, create/bind the instance, set initial values, then create/present the view.
Copy
let worker = try await WorkerProvider.shared.worker()let file = try await File(source: .local("my_rive_file", Bundle.main), worker: worker)let instance = try await file.createViewModelInstance(from: .name("My View Model"))let property = StringProperty(path: "path/to/string")instance.setValue(of: property, to: "Initial Value")let rive = try await Rive(file: file, dataBind: .instance(instance))let riveView = RiveUIView(rive: rive) // Present after initial values are set
For more information, see playback controls in the Apple runtime guide.
Copy
let viewModel = RiveViewModel(fileName: "...")viewModel.pause() // or play() to resume
Copy
let rive = try await Rive(...)let riveView = RiveUIView(rive: rive)riveView.isPaused = true // or false to resume// SwiftUIRiveUIViewRepresentable(rive) .paused(true)
RiveLog.logger = .system(levels: .default)// Or use your own implementation of RiveLog.LoggerRiveLog.logger = MyLogger()// Disable logsRiveLog.logger = .none
The new runtime introduces Worker as an explicit concurrency primitive. This is a substantial change from the legacy runtime, which did not expose an equivalent concept.Benefits include:
Better control over background processing
Shared global asset registration per worker
More predictable architecture when rendering multiple files
A shared worker is usually the best default. Use multiple workers only when you need additional parallel processing, such as when rendering multiple heavyweight graphics.
The new runtime supports async constructors and async view wrappers (RiveUIView with async closure and AsyncRiveUIViewRepresentable) to better model real-world loading flows.
The async wrappers are useful when a view must own its own loading lifecycle. If you need reuse and caching across screens, prefer creating and storing File/Rive objects at a higher level.
Legacy file loading may use CDN-backed asset flows (for example via loadCdn behavior). The new runtime’s asset model is worker-based and push-oriented via explicit asset registration APIs.
The legacy runtime supports state machine input APIs directly. The new runtime does not expose equivalent input APIs, and migration should move to data binding properties (number, boolean, string, trigger-style interactions).The Rive Editor provides a conversion tool in Menu > Convert Inputs to ViewModels that can help with initial migration.
There are no current plans to reintroduce direct state machine input APIs in the new runtime.
The legacy runtime supports event listeners. The new runtime does not currently expose an equivalent event listener API. For many migration scenarios, a data binding contract is the recommended replacement for app-runtime communication.Simple event-like behavior can often be modeled with trigger-oriented view model properties.
There are no current plans to reintroduce legacy-style event listener APIs in the new runtime.
Legacy integrations can directly target linear animations. In migration, graphics are required to contain a state machine.For existing files that rely on linear animations, create a state machine in the Editor that with a single state that plays the desired animation.
Legacy integrations often used state-change delegate callbacks (for example, RiveStateDelegate.stateChange(...)) to react to animation state.The new runtime does not expose a direct state-name observation API in this guide. For migration, model those app-facing signals as data-binding properties and observe them from the bound instance.
Copy
let property = StringProperty(path: "path/to/state_signal")let stream = viewModelInstance.valueStream(of: property)for try await value in stream { // React to state-like changes emitted from the Rive file}
Where possible, prefer named queries and explicit sources (for example, ViewModelSource and named artboard/state machine queries) over index-based coupling.