View models describe a set of properties, but cannot themselves be used to get or set values - that is the role of view model instances.To begin, we need to get a reference to a particular view model. This can be done either by index, by name, or the default for a given artboard, and is done from the Rive file. The default option refers to the view model assigned to an artboard by the dropdown in the editor.
View models are not their own type; rather, they are a source when creating a view model instance from a File.You can define the source of a view model via the ViewModelSource type.
Copy
case artboardDefault(Artboard) // References the default view model for an Artboardcase name(String) // References a view model from a file by name
These sources are used in conjunction with getting a view model instance. See View Model Instances for more information.
Copy
let riveViewModel = RiveViewModel(...)let file = riveViewModel.riveModel!.riveFile// Data binding view model by namelet viewModelByName = file.viewModelNamed("...")// Data binding view model by indexfor index in 0..<file.viewModelCount { let viewModelByIndex = file.viewModel(at: index)}// Default data binding view model for an artboardlet artboard = riveViewModel.riveModel!.artboardlet viewModelForArtboard = file.viewModel(for: artboard)
Once we have a reference to a view model, it can be used to create an instance. When creating an instance, you have four options:
Create a blank instance - Fill the properties of the created instance with default values as follows:
Type
Value
Number
0
String
Empty string
Boolean
False
Color
0xFF000000
Trigger
Untriggered
Enum
The first value
Image
No image
Artboard
No artboard
List
Empty list
Nested view model
Null
Create the default instance - Use the instance labelled “Default” in the editor. Usually this is the one a designer intends as the primary one to be used at runtime.
Create by index - Using the order returned when iterating over all available instances. Useful when creating multiple instances by iteration.
Create by name - Use the editor’s instance name. Useful when creating a specific instance.
In some samples, due to the wordiness of “view model instance”, we use the abbreviation “VMI”, as well as “VM” for “view model”.
The following section assumes that you have read through the Apple overview.
Copy
// From a filelet file: File = ...// When using a view model by name:// A blank view model instancevar blankInstance = try await file.createViewModelInstance(from: .blank(from: .name("ViewModel")))// The default instance for the view modelvar defaultInstance = try await file.createViewModelInstance(from: .name("ViewModel"))// An instance by name from the view modelvar namedInstance = try await file.createViewModelInstance(from: .name("Instance", from: .name("ViewModel")))// Alternatively, using the default view model for an artboardlet artboard: Artboard = ...// A blank view model instanceblankInstance = try await file.createViewModelInstance(from: .blank(from: .artboardDefault(Artboard)))// The default instance for the view modeldefaultInstance = try await file.createViewModelInstance(from: .viewModelDefault(from: .artboardDefault(Artboard)))// An instance by name from the view modelnamedInstance = try await file.createViewModelInstance(from: .name("Instance", from: .artboardDefault(Artboard)))
Copy
let riveViewModel = RiveViewModel(...)let viewModel = riveViewModel.riveModel!.riveFile.viewModelNamed("...")!// Create blanklet blankInstance = viewModel.createInstance()// Create defaultlet defaultInstance = viewModel.createDefaultInstance()// Create by indexfor index in 0..<viewModel.instanceCount { let instanceByIndex = viewModel.createInstance(fromIndex: index)}// Create by namefor name in viewModel.instanceNames { let instanceByName = viewModel.createInstance(fromName: name)}
The created instance can then be assigned to a state machine or artboard. This establishes the bindings set up at edit time.It is preferred to assign to a state machine, as this will automatically apply the instance to the artboard as well. Only assign to an artboard if you are not using a state machine, i.e. your file is static or uses linear animations.
The initial values of the instance are not applied to their bound elements until the state machine or artboard advances.
Alternatively, you can utilize the Rive type to (automatically) data bind a view model instance:
Copy
// Automatically find a default view model instance to bind. This is the default value, if you do not pass in a dataBind argument.var rive = try await Rive(file: file, artboard: artboard, stateMachine: stateMachine, dataBind: .auto)// Bind a view model instancevar rive = try await Rive(file: file, artboard: artboard, stateMachine: stateMachine, dataBind: .viewModelInstance(viewModelInstance))// Do not bind. This assumes you have manually bound a view model instance earliervar rive = try await Rive(file: file, artboard: artboard, stateMachine: stateMachine, dataBind: .none)
Copy
let riveViewModel = RiveViewModel(...)let artboard = riveViewModel.riveModel!.artboard,let instance = riveViewModel.riveModel!.riveFile.defaultViewModel(for: artboard).createDefaultInstance()!// Apply the instance to the state machine (preferred)// Applying to a state machine will automatically bind to its artboardriveViewModel.riveModel!.stateMachine.bind(instance)// Alternatively, apply the instance to the artboardartboard.bind(viewModelInstance: instance)
Alternatively, you may prefer to use auto-binding. This will automatically bind the default view model of the artboard using the default instance to both the state machine and the artboard. The default view model is the one selected on the artboard in the editor dropdown. The default instance is the one marked “Default” in the editor.
When creating a Rive object, you can elect to auto bind:
Copy
// Automatically find a default view model instance to bind. This is the default value, if you do not pass in a dataBind argument.var rive = try await Rive(file: file, artboard: artboard, stateMachine: stateMachine, dataBind: .auto)// orvar rive = try await Rive(file: file, artboard: artboard, stateMachine: stateMachine)
Copy
let riveViewModel = RiveViewModel(...)riveViewModel.riveModel?.enableAutoBind { instance in // Store a reference to `instance` to later access properties // The instance may change as state machines and artboards change}// If you'd like to disable autoBind after enabling…riveViewModel.riveModel!.disableAutoBind()
Property descriptors can be inspected on a view model to discover at runtime which are available. These are not the mutable properties themselves though - once again those are on instances. These descriptors have a type and name.
Copy
let file: File = ...let properties = try await file.getProperties(of: "ViewModel")for property in properties { print(property.type) // enum of string, number, boolean, etc print(property.name) // The name of the property within the view model print(property.metaData) // Additional metadata for the property, if available}
Copy
let riveViewModel = RiveViewModel(...)let viewModel = riveViewModel.riveModel!.file.viewModelNamed(...)!for property in viewModel.properties { print(property.type) // String, number, boolean, etc print(property.name) // The name of the property within the view model}
References to these properties can be retrieved by name or path.Some properties are mutable and have getters, setters, and observer operations for their values. Getting or observing the value will retrieve the latest value set on that property’s binding, as of the last state machine or artboard advance. Setting the value will update the value and all of its bound elements.
After setting a property’s value, the changes will not apply to their bound elements until the state machine or artboard advances.
Property types are a very thin wrapper around the path and return type of a property.All property APIs (e.g setters, getters, and triggers) are available as part of a ViewModelInstance object.
let riveViewModel = RiveViewModel(...)var viewModelInstance: RiveDataBindingViewModel.Instance!// You can get the view model instance when enabling auto bindingriveViewModel.riveModel?.enableAutoBind { instance in // Store a reference to instance viewModelInstance = instance}// Alternatively, you can create a view model instance manuallyviewModelInstance = riveViewModel.riveModel!.riveFile.viewModelNamed("...")!.createDefaultInstance()!// Stringslet stringProperty = instance.stringProperty(fromPath: "...")!// Updating its valuestringProperty.value = "Hello, Rive"// Get its valueprint(stringProperty.value)// You can also set and get values without storing a strong referenceinstance.stringProperty(fromPath: "...").value = "Hello again, Rive"// Numberslet numberProperty = instance.numberProperty(fromPath: "...")!// Updating its valuenumberProperty.value = 1337// Get its valueprint(numberProperty.value)// You can also set and get values without storing a strong referenceinstance.numberProperty(fromPath: "...").value = 1337// Booleanslet booleanProperty = instance.booleanProperty(fromPath: "...")!// Updating its valuebooleanProperty.value = true// Get its valueprint(booleanProperty.value)// You can also set and get values without storing a strong referenceinstance.booleanProperty(fromPath: "...").value = true// Colorslet colorProperty = instance.colorProperty(fromPath: "...")!// Updating its value, which is a UIColor/NSColor, so all static helpers apply.colorProperty.value = .red// Get its valueprint(colorProperty.value)// You can also set and get values without storing a strong referenceinstance.colorProperty(fromPath: "...").value = .red// Enumslet enumProperty = instance.enumProperty(fromPath: "...")!// Updating its valueenumProperty.value = "Foo"// Get its valueprint(enumProperty.value)// Print all possible valuesprint(enumProperty.values)// You can also set and get values without storing a strong referenceinstance.enumProperty(fromPath: "...").value = "Foo"// Triggerlet triggerProperty = instance.triggerProperty(fromPath: "...")!// Fire the triggertriggerProperty.trigger()
View models can have properties of type view model, allowing for arbitrary nesting. You can chain property calls on each instance starting from the root until you get to the property of interest. Alternatively, you can do this through a path parameter, which is similar to a URI in that it is a forward slash delimited list of property names ending in the name of the property of interest.
Property types are no longer reference types, and require the name or full path to a property when initializing the property value type. There is no longer an API to chain nested properties.See Properties for usage details.
Copy
let riveViewModel = RiveViewModel(...)var viewModelInstance: RiveDataBindingViewModel.Instance!// You can get the view model instance when enabling auto bindingriveViewModel.riveModel?.enableAutoBind { instance in // Store a reference to instance viewModelInstance = instance}// Alternatively, you can create a view model instance manuallyviewModelInstance = riveViewModel.riveModel!.riveFile.viewModelNamed("...")!.createDefaultInstance()!let nestedNumberByChain = instance .viewModelInstanceProperty(fromPath: "Nested View Model") .viewModelInstanceProperty(fromPath: "Another Nested View Model") .numberProperty(fromPath: "Number")let nestedNumberByPath = instance.numberProperty(fromPath: "Nested View Model/Another Nested View Model/Number")
You can observe changes over time to property values, either by using listeners or a platform equivalent method. Once observed, you will be notified when the property changes are applied by a state machine advance, whether that is a new value that has been explicitly set or if the value was updated as a result of a binding.
Property listeners utilize Swift Concurrency’s async throwing stream API. If a property returns a value, you can listen to its changes by calling the valueStream(of:) method on a ViewModelInstance object.
Copy
let file: File = ...let viewModelInstance = try await file.createViewModelInstance(...)let stringProperty = StringProperty(path: "path/to/string")let valueStream = viewModelInstance.valueStream(of: stringProperty)do { for try await value in valueStream { print(value) }} catch let error as ViewModelInstanceError { // The thrown error should always be a ViewModelInstanceError type print(error)} catch { print(error)}
For triggers, you can listen to them by calling the stream(of:) method on a ViewModelInstance object. This returns a stream of Void values, which can be ignored.
Copy
let file: File = ...let viewModelInstance = try await file.createViewModelInstance(...)let triggerProperty = TriggerProperty(path: "path/to/trigger")let triggerStream = viewModelInstance.stream(of: triggerProperty)do { for try await _ in triggerStream { print("Trigger fired!") }} catch let error as ViewModelInstanceError { // The thrown error should always be a ViewModelInstanceError type print(error)} catch { print(error)}
Copy
let riveViewModel = RiveViewModel(...)var viewModelInstance: RiveDataBindingViewModel.Instance!// You can get the view model instance when enabling auto bindingriveViewModel.riveModel?.enableAutoBind { instance in // Store a reference to instance viewModelInstance = instance}// Alternatively, you can create a view model instance manuallyviewModelInstance = riveViewModel.riveModel!.riveFile.viewModelNamed("...")!.createDefaultInstance()!// Get the string propertylet stringProperty = instance.stringProperty(fromPath: "...")!// Add a listenerlet listener = stringProperty.addListener { newValue in print(newValue)}// Remove a listener, where listener is the return value of addListenerstringProperty.removeListener(listener)// Trigger properties can also be listened to for when they are triggeredinstance.triggerProperty(fromPath: "...")!.addListener { print("Triggered!")}
Image properties let you set and replace raster images at runtime, with each instance of the image managed independently. For example, you could build an avatar creator and dynamically update features — like swapping out a hat — by setting a view model’s image property.
To set an image, you first need to decode an image from a Worker. This has to be the Worker that was used when initializing a File, from which you are setting the image property of a view model instance.
let riveViewModel = RiveViewModel(...)var viewModelInstance: RiveDataBindingViewModel.Instance!// You can get the view model instance when enabling auto bindingriveViewModel.riveModel?.enableAutoBind { instance in // Store a reference to instance viewModelInstance = instance}// Alternatively, you can create a view model instance manuallyviewModelInstance = riveViewModel.riveModel!.riveFile.viewModelNamed("...")!.createDefaultInstance()!// Create a RiveRenderImage from datalet data = Data(...)var image = RiveRenderImage(data: data)! // This can return nil if the data is not a valid image// Or, create a RiveRenderImage from a UIImageimage = RiveRenderImage(image: UIImage(named: "my_image")!, format: .png)! // This can return nil if the image is not a valid jpg or png imagelet imageProperty = viewModelInstance.imageProperty(fromPath: "image")!// Once you have your data binding view model instance, you can set the image property valueimageProperty.setValue(image)// You can also pass nil to clear the imageimageProperty.setValue(nil)
List properties let you manage a dynamic set of view model instances at runtime. For example, you can build a TODO app where users can add and remove tasks in a scrollable Layout.See the Editor section on creating data bound lists.A single list property can include different view model types, with each view model tied to its own Component, making it easy to populate a list with a varity of Component instances.With list properties, you can:
Add a new view model instance (optionally at an index)
Remove an existing view model instance (optionally by index)
let listProperty = viewModelInstance.listProperty(fromPath: "list")!// Create a new view model instance and add it to the end of the listlet firstInstance = viewModel.createInstanceByName("First Instance")!listProperty.add(firstInstance)// Create a new view model instance and add it to the beginning of the listlet secondInstance = myViewModel.createInstanceByName("Second Instance")!listProperty.add(secondInstance, atIndex: 0)// Swap the first and second instanceslistProperty.swapInstance(atIndex: 0, withInstanceAtIndex: 1)// Remove both instanceslistProperty.removeInstance(secondInstance)listProperty.removeInstance(atIndex: 0)// Get and print the size of the listprint(listProperty.size) // Prints 0
Artboard properties allows you to swap out entire components at runtime. This is useful for creating modular components that can be reused across different designs or applications, for example:
Creating a skinning system that supports a large number of variations, such as a character creator where you can swap out different body parts, clothing, and accessories.
Creating a complex scene that is a composition of various artboards loaded from various different Rive files (drawn to a single canvas/texture/widget).
Reducing the size (complexity) of a single Rive file by breaking it up into smaller components that can be loaded on demand and swapped in and out as needed.
Use the artboardProperty method on a RiveDataBindingViewModel.Instance object to get the artboard property.Then use the setValue method on the artboard property object to set the new artboard value.setValue accepts a RiveBindableArtboard object, which is a wrapper for an artboard that can be used to set the artboard property value.You can get a RiveBindableArtboard object by using the bindableArtboard methods on a RiveFile object.
Enums properties come in two flavors: system and user-defined. In practice, you will not need to worry about the distinction, but just be aware that system enums are available in any Rive file that binds to an editor-defined enum set, representing options from the editor’s dropdowns, where user-defined enums are those defined by a designer in the editor.Enums are string typed. The Rive file contains a list of enums. Each enum in turn has a name and a list of strings.