feat(ios): add support for data binding

Adds support for data binding to the iOS runtime.

## Changes

### RiveFile

Updated to match c++ runtime support for getting view models by index, name, and default for an artboard.

### RiveArtboard

Updated to match c++ runtime support for binding a view model instance.

### RiveStateMachine

Updated to match c++ runtime support for binding a view model instance. Additionally, this holds a strong reference to the view model instance that is currently bound (which aids in knowing which state machine should have its property listeners called after advance).

### RiveModel

Adds support for enabling / disabling autoBind functionality. This has to be done _after_ initialization, since Swift default arguments don't bridge to ObjC, and I didn't want to add n number of new initializers. When enabled, a callback will be called with the bound instance.

## New Types

Currently, type names are similar to those found within the c++ runtime. They're kind of ugly when it comes to ObjC, but the Swift names are cleaned up (e.g `RiveViewModelRuntime.Instance` instead of `RiveViewModelInstanceRuntime`).

### RiveDataBindingViewModel

The bridging type between the c++ runtime equivalent (`rive::ViewModelRuntime`) and ObjC.

### RiveDataBindingViewModelInstance

Swift: `RiveDataBindingViewModel.Instance`

The bridging type between the c++ runtime equivalent (`rive::ViewModelInstanceRuntime`) and ObjC.

### RiveDataBindingViewModelInstanceProperty

Swift: `RiveDataBindingViewModel.Instance.Property`

The superclass bridging type between the c++ runtime equivalent (`rive::ViewModelInstanceValueRuntime`) and ObjC.

### Subclasses

- Strings: `RiveDataBindingViewModelInstanceStringProperty`
  - Swift: `RiveDataBindingViewModel.Instance.StringProperty`
- Numbers: `RiveDataBindingViewModelInstanceNumberProperty`
  -  Swift: `RiveDataBindingViewModel.Instance.NumberProperty`
- Boolean: `RiveDataBindingViewModelInstanceBooleanProperty`
  - Swift: `RiveDataBindingViewModel.Instance.BooleanProperty`
- Color: `RiveDataBindingViewModelInstanceColorProperty`
  - Swift: `RiveDataBindingViewModel.Instance.ColorProperty`
- Enum: `RiveDataBindingViewModelInstanceEnumProperty`
  -  Swift: `RiveDataBindingViewModel.Instance.EnumProperty`
- Trigger: `RiveDataBindingViewModelInstanceTriggerProperty`
  - Swift: `RiveDataBindingViewModel.Instance.TriggerProperty`

### Observability

KVO has (temporarily) been disabled, "forcing" observability to be done through the explicit `addListener` and `removeListener` functions of each property type. `removeListener` exists on the superclass, however the matching `addListener` functions exist on each property type, primarily due to the fact that there is no "pretty" way of handing these functions as generics (where only the value type of the property differs for each callback) other than utilizing `id`. Listeners exist within the context of a property; however, an instance will use its (cached) properties to request that it calls its listeners.

## Details

### Caching properties

When a property getter is called on a view model instance, a cache is first checked for the property, otherwise a new one is returned and cached. This helps with ensuring we are using the same pointer under-the-hood. This isn't strictly necessary (per testing) but does allow for some niceties, such as not having to explicitly maintain a strong reference to a property if you want to just observe: `instance.triggerProperty(from: "...").addListener { ... }`.

Properties are cached for the first component when parsing the path for the property. In the instance that a path with > 1 component  is provided to a property (e.g `instance.triggerProperty(from: 'nested/trigger")`, then the appropriate nested view models are created, and the property is associated with the correct view model (e.g above, the view model `nested will be cached, and the trigger property will be cached within that view model).

### Caching nested view models

Similar to caching properties, when a (nested) view model getter is called on a view model instance, a cache is first checked for the view model, otherwise a new one is returned and cached. This helps ensure that when (re)binding an instance to a (new) state machine or artboard, that all properties within that view model still have its listeners attached, regardless of how nested a path goes. This will likely help with implementing replacing instance functionality in v2.

## Testing

Unit tests have been added for data binding, attempting to capture high-level expectations rather than totally verifying the c++ runtime expectations. This includes things like: all getters returning object-or-nil, listeners being called with the correct values, autoBinding, property and view model caching, etc. The `.riv` file for unit tests is the same one that is used within the Example app.

## Example

A new `.riv` file has been added that shows basic usage of each property type (including observability). The same `.riv` file is used in the unit tests.

Diffs=
b2f1db73d7 feat(ios): add support for data binding (#9227)

Co-authored-by: David Skuza <david@rive.app>
This commit is contained in:
dskuza
2025-04-15 21:45:39 +00:00
parent d48d3d8448
commit 4110f9f3f6
33 changed files with 3566 additions and 9 deletions

View File

@@ -0,0 +1,94 @@
//
// RiveDataBindingViewModel.h
// RiveRuntime
//
// Created by David Skuza on 1/13/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class RiveDataBindingViewModelInstance;
@class RiveDataBindingViewModelInstancePropertyData;
/// An object that represents a View Model of a Rive file.
@interface RiveDataBindingViewModel : NSObject
/// The name of the view model.
@property(nonatomic, readonly) NSString* name;
/// The number of instances in the view model.
@property(nonatomic, readonly) NSUInteger instanceCount;
/// An array of names of all instances in the view model.
@property(nonatomic, readonly) NSArray<NSString*>* instanceNames;
/// The number of all properties in the view model.
@property(nonatomic, readonly) NSUInteger propertyCount;
/// An array of property data of all properties in the view model.
@property(nonatomic, readonly)
NSArray<RiveDataBindingViewModelInstancePropertyData*>* properties;
/// Creates a new instance to bind from a given index.
///
/// The index of an instance starts at 0, where 0 is
/// the first instance that appears in the "Data Bind" panel's instances
/// dropdown.
///
/// - Note: A strong reference to this instance must be maintained if it is
/// being bound to a state machine or artboard, or for observability. Fetching a
/// new instance from the same model, if not bound, will not update its
/// properties when properties are updated.
///
/// - Parameter index: The index of an instance within the view model.
- (nullable RiveDataBindingViewModelInstance*)createInstanceFromIndex:
(NSUInteger)index NS_SWIFT_NAME(createInstance(fromIndex:));
/// Creates a new instance to bind from a given name.
///
/// The name of an instance has to match the name of
/// an instance in the "Data Bind" panel's instances dropdown, where the
/// instance has been exported.
///
/// - Note: A strong reference to this instance must be maintained if it is
/// being bound to a state machine or artboard, or for observability. Fetching a
/// new instance from the same model, if not bound, will not update its
/// properties when properties are updated.
///
/// - Parameter name: The name of an instance within the view model.
- (nullable RiveDataBindingViewModelInstance*)createInstanceFromName:
(NSString*)name NS_SWIFT_NAME(createInstance(fromName:));
/// Creates a new default instance to bind from the view model.
///
/// This is the instance marked as "Default" in the "Data Bind" instances
/// dropdown when an artboard is selected.
///
/// - Note: A strong reference to this instance must be maintained if it is
/// being bound to a state machine or artboard, or for observability. Fetching a
/// new instance from the same model, if not bound, will not update its
/// properties when properties are updated.
- (nullable RiveDataBindingViewModelInstance*)createDefaultInstance;
/// Creates a new instance with Rive default values from the view model to bind
/// to an artboard and/or state machine.
///
/// *Default values*
/// - *String*: ""
/// - *Number*: 0
/// - *Boolean*: false
/// - *Color*: ARGB(0, 0, 0, 0)
/// - *Enum*: An enum's first value
///
/// - Note: A strong reference to this instance must be maintained if it is
/// being bound to a state machine or artboard, or for observability. Fetching a
/// new instance from the same model, if not bound, will not update its
/// properties when properties are updated.
- (nullable RiveDataBindingViewModelInstance*)createInstance;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,135 @@
//
// RiveDataBindingViewModel.m
// RiveRuntime
//
// Created by David Skuza on 1/13/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import <Rive.h>
#import <RivePrivateHeaders.h>
#import <RiveRuntime/RiveRuntime-Swift.h>
@implementation RiveDataBindingViewModel
{
rive::ViewModelRuntime* _viewModel;
}
- (instancetype)initWithViewModel:(rive::ViewModelRuntime*)viewModel
{
if (self = [super init])
{
_viewModel = viewModel;
}
return self;
}
- (void)dealloc
{
_viewModel = nullptr;
}
- (NSString*)name
{
auto name = _viewModel->name();
return [NSString stringWithCString:name.c_str()
encoding:NSUTF8StringEncoding];
}
- (NSUInteger)instanceCount
{
return _viewModel->instanceCount();
}
- (NSArray<NSString*>*)instanceNames
{
auto values = _viewModel->instanceNames();
NSMutableArray* mapped = [NSMutableArray arrayWithCapacity:values.size()];
for (auto it = values.begin(); it != values.end(); ++it)
{
auto name = *it;
NSString* string = [NSString stringWithCString:name.c_str()
encoding:NSUTF8StringEncoding];
[mapped addObject:string];
}
return mapped;
}
- (NSUInteger)propertyCount
{
return _viewModel->propertyCount();
}
- (NSArray<RiveDataBindingViewModelInstancePropertyData*>*)properties
{
auto properties = _viewModel->properties();
NSMutableArray<RiveDataBindingViewModelInstancePropertyData*>* mapped =
[NSMutableArray arrayWithCapacity:properties.size()];
for (auto it = properties.begin(); it != properties.end(); ++it)
{
[mapped addObject:[[RiveDataBindingViewModelInstancePropertyData alloc]
initWithData:*it]];
}
return mapped;
}
- (nullable RiveDataBindingViewModelInstance*)createInstanceFromIndex:
(NSUInteger)index
{
auto instance = _viewModel->createInstanceFromIndex(index);
if (instance == nullptr)
{
[RiveLogger logWithViewModelRuntime:self
createdInstanceFromIndex:index
created:NO];
return nil;
}
[RiveLogger logWithViewModelRuntime:self
createdInstanceFromIndex:index
created:YES];
return [[RiveDataBindingViewModelInstance alloc] initWithInstance:instance];
}
- (nullable RiveDataBindingViewModelInstance*)createInstanceFromName:
(NSString*)name
{
auto instance =
_viewModel->createInstanceFromName(std::string([name UTF8String]));
if (instance == nullptr)
{
[RiveLogger logWithViewModelRuntime:self
createdInstanceFromName:name
created:NO];
return nil;
}
[RiveLogger logWithViewModelRuntime:self
createdInstanceFromName:name
created:YES];
return [[RiveDataBindingViewModelInstance alloc] initWithInstance:instance];
}
- (nullable RiveDataBindingViewModelInstance*)createDefaultInstance
{
auto instance = _viewModel->createDefaultInstance();
if (instance == nullptr)
{
[RiveLogger logViewModelRuntimeCreatedDefaultInstance:self created:NO];
return nil;
}
[RiveLogger logViewModelRuntimeCreatedDefaultInstance:self created:YES];
return [[RiveDataBindingViewModelInstance alloc] initWithInstance:instance];
}
- (nullable RiveDataBindingViewModelInstance*)createInstance
{
auto instance = _viewModel->createInstance();
if (instance == nullptr)
{
[RiveLogger logViewModelRuntimeCreatedInstance:self created:NO];
return nil;
}
[RiveLogger logViewModelRuntimeCreatedInstance:self created:YES];
return [[RiveDataBindingViewModelInstance alloc] initWithInstance:instance];
}
@end

View File

@@ -0,0 +1,153 @@
//
// RiveDataBindingViewModelInstance.h
// RiveRuntime
//
// Created by David Skuza on 1/13/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class RiveDataBindingViewModelInstanceProperty;
@class RiveDataBindingViewModelInstanceStringProperty;
@class RiveDataBindingViewModelInstanceNumberProperty;
@class RiveDataBindingViewModelInstanceBooleanProperty;
@class RiveDataBindingViewModelInstanceColorProperty;
@class RiveDataBindingViewModelInstanceEnumProperty;
@class RiveDataBindingViewModelInstanceTriggerProperty;
@class RiveDataBindingViewModelInstancePropertyData;
/// An object that represents an instance of a view model, used to update
/// bindings at runtime.
///
/// - Note: A strong reference to this instance must be maintained if it is
/// being bound to a state machine or artboard, or for observability. If a
/// property is fetched from an instance different to that bound to an artboard
/// or state machine, its value or trigger will not be updated.
NS_SWIFT_NAME(RiveDataBindingViewModel.Instance)
@interface RiveDataBindingViewModelInstance : NSObject
/// The name of the view model instance.
@property(nonatomic, readonly) NSString* name;
/// The number of all properties in the view model instance.
@property(nonatomic, readonly) NSUInteger propertyCount;
/// An array of property data of all properties in the view model instance.
@property(nonatomic, readonly)
NSArray<RiveDataBindingViewModelInstancePropertyData*>* properties;
/// Gets a property from the view model instance. This property is the
/// superclass of all other property types.
///
/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type
/// does not have to be made. If the property exists, the underlying property
/// will be cached, and calling this function again with the same path is
/// guaranteed to return the same object.
///
/// - Parameter path: The path to the property.
///
/// - Returns: The property if it exists at the supplied path, otherwise nil.
- (nullable RiveDataBindingViewModelInstanceProperty*)propertyFromPath:
(NSString*)path;
/// Gets a string property in the view model instance.
///
/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type
/// does not have to be made. If the property exists, the underlying property
/// will be cached, and calling this function again with the same path is
/// guaranteed to return the same object.
///
/// - Parameter path: The path to the string property.
///
/// - Returns: The property if it exists at the supplied path, otherwise nil.
- (nullable RiveDataBindingViewModelInstanceStringProperty*)
stringPropertyFromPath:(NSString*)path;
/// Gets a number property in the view model instance.
///
/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type
/// does not have to be made. If the property exists, the underlying property
/// will be cached, and calling this function again with the same path is
/// guaranteed to return the same object.
///
/// - Parameter path: The path to the number property.
///
/// - Returns: The property if it exists at the supplied path, otherwise nil.
- (nullable RiveDataBindingViewModelInstanceNumberProperty*)
numberPropertyFromPath:(NSString*)path;
/// Gets a boolean property in the view model instance.
///
/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type
/// does not have to be made. If the property exists, the underlying property
/// will be cached, and calling this function again with the same path is
/// guaranteed to return the same object.
///
/// - Parameter path: The path to the number property.
///
/// - Returns: The property if it exists at the supplied path, otherwise nil.
- (nullable RiveDataBindingViewModelInstanceBooleanProperty*)
booleanPropertyFromPath:(NSString*)path;
/// Gets a color property in the view model instance.
///
/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type
/// does not have to be made. If the property exists, the underlying property
/// will be cached, and calling this function again with the same path is
/// guaranteed to return the same object.
///
/// - Parameter path: The path to the number property.
///
/// - Returns: The property if it exists at the supplied path, otherwise nil.
- (nullable RiveDataBindingViewModelInstanceColorProperty*)
colorPropertyFromPath:(NSString*)path;
/// Gets a enum property in the view model instance.
///
/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type
/// does not have to be made. If the property exists, the underlying property
/// will be cached, and calling this function again with the same path is
/// guaranteed to return the same object.
///
/// - Parameter path: The path to the number property.
///
/// - Returns: The property if it exists at the supplied path, otherwise nil.
- (nullable RiveDataBindingViewModelInstanceEnumProperty*)enumPropertyFromPath:
(NSString*)path;
/// Gets a view model property in the view model instance.
///
/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type
/// does not have to be made. If the property exists, the underlying property
/// will be cached, and calling this function again with the same path is
/// guaranteed to return the same object.
///
/// - Parameter path: The path to the number property.
///
/// - Returns: The property if it exists at the supplied path, otherwise nil.
- (nullable RiveDataBindingViewModelInstance*)viewModelInstancePropertyFromPath:
(NSString*)path;
/// Returns a trigger property in the view model instance.
///
/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type
/// does not have to be made. If the property exists, the underlying property
/// will be cached, and calling this function again with the same path is
/// guaranteed to return the same object.
///
/// - Parameter path: The path to the number property.
///
/// - Returns: The property if it exists at the supplied path, otherwise nil.
- (nullable RiveDataBindingViewModelInstanceTriggerProperty*)
triggerPropertyFromPath:(NSString*)path;
/// Calls all registered property listeners for the properties of the view model
/// instance.
- (void)updateListeners;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,430 @@
//
// RiveDataBindingViewModelInstance.m
// RiveRuntime
//
// Created by David Skuza on 1/13/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import <Rive.h>
#import <RivePrivateHeaders.h>
#import <RiveRuntime/RiveRuntime-Swift.h>
@interface RiveDataBindingViewModelInstance () <
RiveDataBindingViewModelInstancePropertyDelegate>
@end
@implementation RiveDataBindingViewModelInstance
{
rive::ViewModelInstanceRuntime* _instance;
NSMutableDictionary<NSString*, RiveDataBindingViewModelInstanceProperty*>*
_properties;
NSMutableDictionary<NSString*, RiveDataBindingViewModelInstance*>*
_children;
}
- (instancetype)initWithInstance:(rive::ViewModelInstanceRuntime*)instance
{
if (self = [super init])
{
_instance = instance;
_properties = [NSMutableDictionary dictionary];
_children = [NSMutableDictionary dictionary];
}
return self;
}
- (void)dealloc
{
_instance = nullptr;
}
- (NSString*)name
{
return [NSString stringWithCString:_instance->name().c_str()
encoding:NSUTF8StringEncoding];
}
- (NSUInteger)propertyCount
{
return _instance->propertyCount();
}
- (NSArray<RiveDataBindingViewModelInstancePropertyData*>*)properties
{
auto properties = _instance->properties();
NSMutableArray<RiveDataBindingViewModelInstancePropertyData*>* mapped =
[NSMutableArray arrayWithCapacity:properties.size()];
for (auto it = properties.begin(); it != properties.end(); ++it)
{
[mapped addObject:[[RiveDataBindingViewModelInstancePropertyData alloc]
initWithData:*it]];
}
return mapped;
}
- (nullable RiveDataBindingViewModelInstanceProperty*)propertyFromPath:
(NSString*)path
{
RiveDataBindingViewModelInstanceProperty* cached;
if ((cached = [self
cachedPropertyFromPath:path
asClass:[RiveDataBindingViewModelInstanceProperty
class]]))
{
return cached;
}
auto property = _instance->property(std::string([path UTF8String]));
if (property == nullptr)
{
[RiveLogger logWithViewModelInstance:self propertyAtPath:path found:NO];
return nil;
}
[RiveLogger logWithViewModelInstance:self propertyAtPath:path found:YES];
RiveDataBindingViewModelInstanceProperty* value =
[[RiveDataBindingViewModelInstanceProperty alloc]
initWithValue:property];
value.valueDelegate = self;
[self cacheProperty:value withPath:path];
return value;
}
- (nullable RiveDataBindingViewModelInstanceStringProperty*)
stringPropertyFromPath:(NSString*)path
{
RiveDataBindingViewModelInstanceStringProperty* cached;
if ((cached = [self
cachedPropertyFromPath:path
asClass:
[RiveDataBindingViewModelInstanceStringProperty
class]]))
{
return cached;
}
auto string = _instance->propertyString(std::string([path UTF8String]));
if (string == nullptr)
{
[RiveLogger logWithViewModelInstance:self
stringPropertyAtPath:path
found:NO];
return nil;
}
[RiveLogger logWithViewModelInstance:self
stringPropertyAtPath:path
found:YES];
RiveDataBindingViewModelInstanceStringProperty* stringValue =
[[RiveDataBindingViewModelInstanceStringProperty alloc]
initWithString:string];
stringValue.valueDelegate = self;
[self cacheProperty:stringValue withPath:path];
return stringValue;
}
- (RiveDataBindingViewModelInstanceNumberProperty*)numberPropertyFromPath:
(NSString*)path
{
RiveDataBindingViewModelInstanceNumberProperty* cached;
if ((cached = [self
cachedPropertyFromPath:path
asClass:
[RiveDataBindingViewModelInstanceNumberProperty
class]]))
{
return cached;
}
auto number = _instance->propertyNumber(std::string([path UTF8String]));
if (number == nullptr)
{
[RiveLogger logWithViewModelInstance:self
numberPropertyAtPath:path
found:NO];
return nil;
}
[RiveLogger logWithViewModelInstance:self
numberPropertyAtPath:path
found:YES];
RiveDataBindingViewModelInstanceNumberProperty* numberValue =
[[RiveDataBindingViewModelInstanceNumberProperty alloc]
initWithNumber:number];
numberValue.valueDelegate = self;
[self cacheProperty:numberValue withPath:path];
return numberValue;
}
- (RiveDataBindingViewModelInstanceBooleanProperty*)booleanPropertyFromPath:
(NSString*)path
{
RiveDataBindingViewModelInstanceBooleanProperty* cached;
if ((cached = [self
cachedPropertyFromPath:path
asClass:
[RiveDataBindingViewModelInstanceBooleanProperty
class]]))
{
return cached;
}
auto boolean = _instance->propertyBoolean(std::string([path UTF8String]));
if (boolean == nullptr)
{
[RiveLogger logWithViewModelInstance:self
booleanPropertyAtPath:path
found:NO];
return nil;
}
[RiveLogger logWithViewModelInstance:self
booleanPropertyAtPath:path
found:YES];
RiveDataBindingViewModelInstanceBooleanProperty* boolValue =
[[RiveDataBindingViewModelInstanceBooleanProperty alloc]
initWithBoolean:boolean];
boolValue.valueDelegate = self;
[self cacheProperty:boolValue withPath:path];
return boolValue;
}
- (RiveDataBindingViewModelInstanceColorProperty*)colorPropertyFromPath:
(NSString*)path
{
RiveDataBindingViewModelInstanceColorProperty* cached;
if ((cached = [self
cachedPropertyFromPath:path
asClass:
[RiveDataBindingViewModelInstanceColorProperty
class]]))
{
return cached;
}
auto color = _instance->propertyColor(std::string([path UTF8String]));
if (color == nullptr)
{
[RiveLogger logWithViewModelInstance:self
colorPropertyAtPath:path
found:NO];
return nil;
}
[RiveLogger logWithViewModelInstance:self
colorPropertyAtPath:path
found:YES];
RiveDataBindingViewModelInstanceColorProperty* colorValue =
[[RiveDataBindingViewModelInstanceColorProperty alloc]
initWithColor:color];
colorValue.valueDelegate = self;
[self cacheProperty:colorValue withPath:path];
return colorValue;
}
- (RiveDataBindingViewModelInstanceEnumProperty*)enumPropertyFromPath:
(NSString*)path
{
RiveDataBindingViewModelInstanceEnumProperty* cached;
if ((cached = [self
cachedPropertyFromPath:path
asClass:
[RiveDataBindingViewModelInstanceEnumProperty
class]]))
{
return cached;
}
auto e = _instance->propertyEnum(std::string([path UTF8String]));
if (e == nullptr)
{
[RiveLogger logWithViewModelInstance:self
enumPropertyAtPath:path
found:NO];
return nil;
}
[RiveLogger logWithViewModelInstance:self
enumPropertyAtPath:path
found:YES];
RiveDataBindingViewModelInstanceEnumProperty* enumProperty =
[[RiveDataBindingViewModelInstanceEnumProperty alloc] initWithEnum:e];
enumProperty.valueDelegate = self;
[self cacheProperty:enumProperty withPath:path];
return enumProperty;
}
- (RiveDataBindingViewModelInstance*)viewModelInstancePropertyFromPath:
(NSString*)path
{
return [self childForPath:path];
}
- (RiveDataBindingViewModelInstanceTriggerProperty*)triggerPropertyFromPath:
(NSString*)path
{
RiveDataBindingViewModelInstanceTriggerProperty* cached;
if ((cached = [self
cachedPropertyFromPath:path
asClass:
[RiveDataBindingViewModelInstanceTriggerProperty
class]]))
{
return cached;
}
auto trigger = _instance->propertyTrigger(std::string([path UTF8String]));
if (trigger == nullptr)
{
[RiveLogger logWithViewModelInstance:self
triggerPropertyAtPath:path
found:NO];
return nil;
}
[RiveLogger logWithViewModelInstance:self
triggerPropertyAtPath:path
found:YES];
RiveDataBindingViewModelInstanceTriggerProperty* triggerProperty =
[[RiveDataBindingViewModelInstanceTriggerProperty alloc]
initWithTrigger:trigger];
triggerProperty.valueDelegate = self;
[self cacheProperty:triggerProperty withPath:path];
return triggerProperty;
}
- (void)updateListeners
{
[_properties enumerateKeysAndObjectsUsingBlock:^(
NSString* _Nonnull key,
RiveDataBindingViewModelInstanceProperty* _Nonnull obj,
BOOL* _Nonnull stop) {
if (obj.hasChanged)
{
[obj handleListeners];
}
}];
[_properties enumerateKeysAndObjectsUsingBlock:^(
NSString* _Nonnull key,
RiveDataBindingViewModelInstanceProperty* _Nonnull obj,
BOOL* _Nonnull stop) {
if (obj.hasChanged)
{
[obj clearChanges];
}
}];
[_children enumerateKeysAndObjectsUsingBlock:^(
NSString* _Nonnull key,
RiveDataBindingViewModelInstance* _Nonnull obj,
BOOL* _Nonnull stop) {
[obj updateListeners];
}];
}
#pragma mark Private
- (rive::ViewModelInstanceRuntime*)instance
{
return _instance;
}
- (void)cacheProperty:(RiveDataBindingViewModelInstanceProperty*)value
withPath:(NSString*)path
{
NSArray<NSString*>* components = [path pathComponents];
if (components.count == 1)
{
_properties[path] = value;
}
else
{
RiveDataBindingViewModelInstance* child =
[self childForPath:components[0]];
if (child)
{
NSArray* subcomponents = [components
subarrayWithRange:NSMakeRange(1, components.count - 1)];
NSString* subpath = [subcomponents componentsJoinedByString:@"/"];
[child cacheProperty:value withPath:subpath];
}
}
}
- (nullable id)cachedPropertyFromPath:(NSString*)path asClass:(Class)aClass
{
RiveDataBindingViewModelInstanceProperty* property =
[_properties objectForKey:path];
if (property != nil && [property isKindOfClass:aClass])
{
return property;
}
return nil;
}
#pragma mark - RiveDataBindingViewModelInstancePropertyDelegate
- (void)valuePropertyDidAddListener:
(RiveDataBindingViewModelInstanceProperty*)value
{}
- (void)valuePropertyDidRemoveListener:
(RiveDataBindingViewModelInstanceProperty*)value
isEmpty:(BOOL)isEmpty
{}
#pragma mark - Paths
- (nullable RiveDataBindingViewModelInstance*)childForPath:(NSString*)path
{
NSArray* components = [path pathComponents];
// If we have no components, we have no child to add
if (components.count == 0)
{
return nil;
}
// E.g from "current/path", this is "current".
// If a child exists with that name, return it.
NSString* currentPath = components[0];
// Use map over set
RiveDataBindingViewModelInstance* existing = nil;
if ((existing = [_children objectForKey:currentPath]))
{
return existing;
}
// Otherwise, for the current path, build a tree recursively, starting with
// the current position.
auto instance =
_instance->propertyViewModel(std::string([currentPath UTF8String]));
if (instance == nullptr)
{
return nil;
}
RiveDataBindingViewModelInstance* child =
[[RiveDataBindingViewModelInstance alloc] initWithInstance:instance];
_children[currentPath] = child;
if (components.count == 1)
{
return child;
}
else
{
NSArray* subpath =
[components subarrayWithRange:NSMakeRange(1, components.count - 1)];
return [self childForPath:[subpath componentsJoinedByString:@"/"]];
}
}
@end

View File

@@ -0,0 +1,265 @@
//
// RiveDataBindingViewModelInstanceStringProperty.h
// RiveRuntime
//
// Created by David Skuza on 1/13/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import <Foundation/Foundation.h>
#if TARGET_OS_OSX
#import <AppKit/NSColor.h>
#define RiveDataBindingViewModelInstanceColor NSColor
#else
#import <UIKit/UIColor.h>
#define RiveDataBindingViewModelInstanceColor UIColor
#endif
NS_ASSUME_NONNULL_BEGIN
/// An object that represents a property of a view model instance.
NS_SWIFT_NAME(RiveDataBindingViewModelInstance.Property)
@interface RiveDataBindingViewModelInstanceProperty : NSObject
/// The name of the property.
@property(nonatomic, readonly) NSString* name;
/// Returns whether the property has changed, and the change will be reflected
/// on next advance.
@property(nonatomic, readonly) BOOL hasChanged;
- (instancetype)init NS_UNAVAILABLE;
/// Resets a property's changed status, resetting `hasChanged` to false.
- (void)clearChanges;
/// Removes a listener for the property.
///
/// - Parameter listener: The listener to remove. This value will be returned by
/// the matching call to `addListener`.
- (void)removeListener:(NSUUID*)listener;
@end
#pragma mark - String
typedef void (^RiveDataBindingViewModelInstanceStringPropertyListener)(
NSString*)
NS_SWIFT_NAME(RiveDataBindingViewModelInstanceStringProperty.Listener);
/// An object that represents a string property of a view model instance.
NS_SWIFT_NAME(RiveDataBindingViewModelInstance.StringProperty)
@interface RiveDataBindingViewModelInstanceStringProperty
: RiveDataBindingViewModelInstanceProperty
/// The string value of the property.
@property(nonatomic, copy) NSString* value;
- (instancetype)init NS_UNAVAILABLE;
/// Adds a block as a listener, called with the latest value when value is
/// updated.
///
/// - Note: The value can be updated either explicitly by the developer,
/// or as a result of a change in a state machine.
///
/// - Parameter listener: The block that will be called when the property's
/// value changes.
///
/// - Returns: A UUID for the listener, used in conjunction with
/// `removeListener`.
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceStringPropertyListener)listener;
@end
#pragma mark - Number
typedef void (^RiveDataBindingViewModelInstanceNumberPropertyListener)(float)
NS_SWIFT_NAME(RiveDataBindingViewModelInstanceNumberProperty.Listener);
/// An object that represents a number property of a view model instance.
NS_SWIFT_NAME(RiveDataBindingViewModelInstance.NumberProperty)
@interface RiveDataBindingViewModelInstanceNumberProperty
: RiveDataBindingViewModelInstanceProperty
/// The number value of the property.
@property(nonatomic, assign) float value;
- (instancetype)init NS_UNAVAILABLE;
/// Adds a block as a listener, called with the latest value when value is
/// updated.
///
/// - Note: The value can be updated either explicitly by the developer,
/// or as a result of a change in a state machine.
///
/// - Parameter listener: The block that will be called when the property's
/// value changes.
///
/// - Returns: A UUID for the listener, used in conjunction with
/// `removeListener`.
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceNumberPropertyListener)listener;
@end
#pragma mark - Boolean
typedef void (^RiveDataBindingViewModelInstanceBooleanPropertyListener)(BOOL)
NS_SWIFT_NAME(RiveDataBindingViewModelInstanceBooleanProperty.Listener);
/// An object that represents a boolean property of a view model instance.
NS_SWIFT_NAME(RiveDataBindingViewModelInstance.BooleanProperty)
@interface RiveDataBindingViewModelInstanceBooleanProperty
: RiveDataBindingViewModelInstanceProperty
/// The boolean value of the property.
@property(nonatomic, assign) BOOL value;
- (instancetype)init NS_UNAVAILABLE;
/// Adds a block as a listener, called with the latest value when value is
/// updated.
///
/// - Note: The value can be updated either explicitly by the developer,
/// or as a result of a change in a state machine.
///
/// - Parameter listener: The block that will be called when the property's
/// value changes.
///
/// - Returns: A UUID for the listener, used in conjunction with
/// `removeListener`.
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceBooleanPropertyListener)listener;
@end
#pragma mark - Color
typedef void (^RiveDataBindingViewModelInstanceColorPropertyListener)(
RiveDataBindingViewModelInstanceColor*)
NS_SWIFT_NAME(RiveDataBindingViewModelInstanceColorProperty.Listener);
/// An object that represents a color property of a view model instance.
NS_SWIFT_NAME(RiveDataBindingViewModelInstance.ColorProperty)
@interface RiveDataBindingViewModelInstanceColorProperty
: RiveDataBindingViewModelInstanceProperty
/// The color value of the property as an integer, as 0xAARRGGBB.
@property(nonatomic, copy) RiveDataBindingViewModelInstanceColor* value;
- (instancetype)init NS_UNAVAILABLE;
/// Sets a new color value based on RGB values, preserving its alpha value.
/// - Parameters:
/// - red: The red value of the color (0-255).
/// - green: The green value of the color (0-255)
/// - blue: The blue value of the color (0-255)
- (void)setRed:(CGFloat)red
green:(CGFloat)green
blue:(CGFloat)blue NS_SWIFT_NAME(set(red:green:blue:));
/// Sets a new color value based on alpha and RGB values.
/// - Parameters:
/// - red: The red value of the color (0-255).
/// - green: The green value of the color (0-255)
/// - blue: The blue value of the color (0-255)
/// - alpha: The alpha value of the color (0-255)
- (void)setRed:(CGFloat)red
green:(CGFloat)green
blue:(CGFloat)blue
alpha:(CGFloat)alpha NS_SWIFT_NAME(set(red:green:blue:alpha:));
/// Sets a new alpha value, preserving the current color.
- (void)setAlpha:(CGFloat)alpha;
/// Adds a block as a listener, called with the latest value when value is
/// updated.
///
/// - Note: The value can be updated either explicitly by the developer,
/// or as a result of a change in a state machine.
///
/// - Parameter listener: The block that will be called when the property's
/// value changes.
///
/// - Returns: A UUID for the listener, used in conjunction with
/// `removeListener`.
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceColorPropertyListener)listener;
@end
#pragma mark - Enum
typedef void (^RiveDataBindingViewModelInstanceEnumPropertyListener)(NSString*)
NS_SWIFT_NAME(RiveDataBindingViewModelInstanceEnumProperty.Listener);
/// An object that represents an enum property of a view model instance.
NS_SWIFT_NAME(RiveDataBindingViewModelInstance.EnumProperty)
@interface RiveDataBindingViewModelInstanceEnumProperty
: RiveDataBindingViewModelInstanceProperty
/// The current string value of the enum property.
@property(nonatomic, copy) NSString* value;
/// An array of all possible values for the enum.
@property(nonatomic, readonly) NSArray<NSString*>* values;
/// The index of the current value in `values`. Setting a new index will also
/// update the `value` of this property.
///
/// - Note: If the new index is outside of the bounds of `values`, this will do
/// nothing, or return 0.
@property(nonatomic, assign) int valueIndex;
- (instancetype)init NS_UNAVAILABLE;
/// Adds a block as a listener, called with the latest value when value is
/// updated.
///
/// - Note: The value can be updated either explicitly by the developer,
/// or as a result of a change in a state machine.
///
/// - Parameter listener: The block that will be called when the property's
/// value changes.
///
/// - Returns: A UUID for the listener, used in conjunction with
/// `removeListener`.
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceEnumPropertyListener)listener;
@end
#pragma mark - Trigger
typedef void (^RiveDataBindingViewModelInstanceTriggerPropertyListener)(void)
NS_SWIFT_NAME(RiveDataBindingViewModelInstanceTriggerProperty.Listener);
/// An object that represents a trigger property of a view model instance.
NS_SWIFT_NAME(RiveDataBindingViewModelInstance.TriggerProperty)
@interface RiveDataBindingViewModelInstanceTriggerProperty
: RiveDataBindingViewModelInstanceProperty
- (instancetype)init NS_UNAVAILABLE;
/// Triggers a trigger property.
- (void)trigger;
/// Adds a block as a listener, called when the property is triggered.
///
/// - Note: The property can be triggered either explicitly by the developer,
/// or as a result of a change in a state machine.
///
/// - Parameter listener: The block that will be called when the property's
/// value changes.
///
/// - Returns: A UUID for the listener, used in conjunction with
/// `removeListener`.
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceTriggerPropertyListener)listener;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,538 @@
//
// RiveDataBindingViewModelInstanceProperty.m
// RiveRuntime
//
// Created by David Skuza on 1/13/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import <Rive.h>
#import <RivePrivateHeaders.h>
#import <RiveRuntime/RiveRuntime-Swift.h>
#import <objc/runtime.h>
#import <WeakContainer.h>
@interface RiveDataBindingViewModelInstancePropertyListener<ValueType>
: NSObject
@property(nonatomic, readonly) void (^listener)(ValueType);
- (instancetype)initWithListener:(void (^)(ValueType))listener;
@end
#pragma mark - String
@implementation RiveDataBindingViewModelInstanceProperty
{
rive::ViewModelInstanceValueRuntime* _value;
NSUUID* _uuid;
NSMutableDictionary<NSUUID*, id>* _listeners;
WeakContainer<id<RiveDataBindingViewModelInstancePropertyDelegate>>*
_delegateContainer;
}
- (instancetype)initWithValue:(rive::ViewModelInstanceValueRuntime*)value
{
if (self = [super init])
{
_value = value;
_uuid = [NSUUID UUID];
_listeners = [NSMutableDictionary dictionary];
}
return self;
}
- (void)dealloc
{
_value = nullptr;
if (self.valueDelegate != nil)
{
[self.valueDelegate valuePropertyDidRemoveListener:self isEmpty:YES];
}
}
- (NSString*)name
{
return [NSString stringWithCString:_value->name().c_str()
encoding:NSUTF8StringEncoding];
}
- (BOOL)hasValue
{
return [self respondsToSelector:@selector(value)];
}
- (BOOL)hasChanged
{
return _value->hasChanged();
}
- (void)clearChanges
{
_value->clearChanges();
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString*)key
{
return NO;
}
- (NSDictionary<NSUUID*, id>*)listeners
{
return _listeners;
}
- (NSUUID*)addListener:(id)listener
{
NSUUID* uuid = [NSUUID UUID];
_listeners[uuid] = [listener copy];
if (self.valueDelegate)
{
[self.valueDelegate valuePropertyDidAddListener:self];
}
return uuid;
}
- (void)removeListener:(NSUUID*)listener
{
_listeners[listener] = nil;
if (self.valueDelegate)
{
[self.valueDelegate
valuePropertyDidRemoveListener:self
isEmpty:(_listeners.count == 0)];
}
}
- (void)handleListeners
{
NSAssert(
NO, @"handleListeners is not implemented by a subclass of this class.");
}
#pragma mark NSCopying
- (BOOL)isEqual:(id)other
{
if (other == self)
{
return YES;
}
else if ([other isKindOfClass:[self class]])
{
return ((RiveDataBindingViewModelInstanceProperty*)other).hash ==
self.hash;
}
else if (![super isEqual:other])
{
return NO;
}
else
{
return NO;
}
}
- (NSUInteger)hash
{
return _uuid.hash;
}
#pragma mark Private
- (nullable id<RiveDataBindingViewModelInstancePropertyDelegate>)valueDelegate
{
return [_delegateContainer object];
}
- (void)setValueDelegate:
(id<RiveDataBindingViewModelInstancePropertyDelegate>)delegate
{
WeakContainer* container = [[WeakContainer alloc] init];
container.object = delegate;
_delegateContainer = container;
}
@end
@implementation RiveDataBindingViewModelInstanceStringProperty
{
rive::ViewModelInstanceStringRuntime* _string;
}
- (instancetype)initWithString:(rive::ViewModelInstanceStringRuntime*)string
{
if (self = [super initWithValue:string])
{
_string = string;
}
return self;
}
- (void)dealloc
{
_string = nullptr;
}
- (void)setValue:(NSString*)value
{
_string->value(std::string([value UTF8String]));
[RiveLogger logPropertyUpdated:self value:value];
}
- (NSString*)value
{
auto value = _string->value();
return [NSString stringWithCString:value.c_str()
encoding:NSUTF8StringEncoding];
}
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceStringPropertyListener)listener
{
return [super addListener:listener];
}
- (void)handleListeners
{
for (RiveDataBindingViewModelInstanceStringPropertyListener listener in self
.listeners.allValues)
{
listener(self.value);
}
}
@end
#pragma mark - Number
@implementation RiveDataBindingViewModelInstanceNumberProperty
{
rive::ViewModelInstanceNumberRuntime* _number;
}
- (instancetype)initWithNumber:(rive::ViewModelInstanceNumberRuntime*)number
{
if (self = [super initWithValue:number])
{
_number = number;
}
return self;
}
- (void)dealloc
{
_number = nullptr;
}
- (void)setValue:(float)value
{
_number->value(value);
[RiveLogger logPropertyUpdated:self
value:[NSString stringWithFormat:@"%f", value]];
}
- (float)value
{
return _number->value();
}
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceNumberPropertyListener)listener
{
return [super addListener:listener];
}
- (void)handleListeners
{
for (RiveDataBindingViewModelInstanceNumberPropertyListener listener in self
.listeners.allValues)
{
listener(self.value);
}
}
@end
#pragma mark - Boolean
@implementation RiveDataBindingViewModelInstanceBooleanProperty
{
rive::ViewModelInstanceBooleanRuntime* _boolean;
}
- (instancetype)initWithBoolean:(rive::ViewModelInstanceBooleanRuntime*)boolean
{
if (self = [super initWithValue:boolean])
{
_boolean = boolean;
}
return self;
}
- (void)dealloc
{
_boolean = nullptr;
}
- (void)setValue:(BOOL)value
{
_boolean->value(value);
[RiveLogger
logPropertyUpdated:self
value:[NSString
stringWithFormat:@"%@",
value ? @"true" : @"false"]];
}
- (BOOL)value
{
return _boolean->value();
}
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceBooleanPropertyListener)listener
{
return [super addListener:listener];
}
- (void)handleListeners
{
for (RiveDataBindingViewModelInstanceBooleanPropertyListener listener in
self.listeners.allValues)
{
listener(self.value);
}
}
@end
#pragma mark - Color
@implementation RiveDataBindingViewModelInstanceColorProperty
{
rive::ViewModelInstanceColorRuntime* _color;
}
- (instancetype)initWithColor:(rive::ViewModelInstanceColorRuntime*)color
{
if (self = [super initWithValue:color])
{
_color = color;
}
return self;
}
- (void)dealloc
{
_color = nullptr;
}
- (RiveDataBindingViewModelInstanceColor*)value
{
int value = _color->value();
CGFloat a = ((CGFloat)((value >> 24) & 0xFF)) / 255;
CGFloat r = ((CGFloat)((value >> 16) & 0xFF)) / 255;
CGFloat g = ((CGFloat)((value >> 8) & 0xFF)) / 255;
CGFloat b = ((CGFloat)(value & 0xFF)) / 255;
return [RiveDataBindingViewModelInstanceColor colorWithRed:r
green:g
blue:b
alpha:a];
}
- (void)setValue:(RiveDataBindingViewModelInstanceColor*)value
{
CGFloat a;
CGFloat r;
CGFloat g;
CGFloat b;
[value getRed:&r green:&g blue:&b alpha:&a];
int intA = (int)(a * 255) << 24;
int intR = (int)(r * 255) << 16;
int intG = (int)(g * 255) << 8;
int intB = (int)(b * 255);
int color = intA | intR | intG | intB;
_color->value(color);
[RiveLogger
logPropertyUpdated:self
value:[NSString stringWithFormat:@"(Color: %@)", value]];
}
- (void)setRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue
{
CGFloat r = fmax(0, fmin(red, 1.0));
CGFloat g = fmax(0, fmin(green, 1.0));
CGFloat b = fmax(0, fmin(blue, 1.0));
_color->rgb((int)(r * 255), (int)(g * 255), (int)(b * 255));
[RiveLogger
logPropertyUpdated:self
value:[NSString stringWithFormat:@"(R: %f, G: %f, B: %f)",
red,
green,
blue]];
}
- (void)setRed:(CGFloat)red
green:(CGFloat)green
blue:(CGFloat)blue
alpha:(CGFloat)alpha
{
CGFloat r = fmax(0, fmin(red, 1.0));
CGFloat g = fmax(0, fmin(green, 1.0));
CGFloat b = fmax(0, fmin(blue, 1.0));
CGFloat a = fmax(0, fmin(alpha, 1.0));
_color->argb(
(int)(a * 255), (int)(r * 255), (int)(g * 255), (int)(b * 255));
[RiveLogger
logPropertyUpdated:self
value:[NSString
stringWithFormat:@"(A: %f, R: %f, G: %f, B: %f)",
alpha,
red,
green,
blue]];
}
- (void)setAlpha:(CGFloat)alpha
{
CGFloat a = fmax(0, fmin(alpha, 1.0));
_color->alpha((int)(a * 255));
[RiveLogger
logPropertyUpdated:self
value:[NSString stringWithFormat:@"(A: %lf)", alpha]];
}
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceColorPropertyListener)listener
{
return [super addListener:listener];
}
- (void)handleListeners
{
for (RiveDataBindingViewModelInstanceColorPropertyListener listener in self
.listeners.allValues)
{
listener(self.value);
}
}
@end
#pragma mark - Enum
@implementation RiveDataBindingViewModelInstanceEnumProperty
{
rive::ViewModelInstanceEnumRuntime* _enum;
}
- (instancetype)initWithEnum:(rive::ViewModelInstanceEnumRuntime*)e
{
if (self = [super initWithValue:e])
{
_enum = e;
}
return self;
}
- (void)dealloc
{
_enum = nullptr;
}
- (NSString*)value
{
auto value = _enum->value();
return [NSString stringWithCString:value.c_str()
encoding:NSUTF8StringEncoding];
}
- (void)setValue:(NSString*)value
{
_enum->value(std::string([value UTF8String]));
[RiveLogger logPropertyUpdated:self value:value];
}
- (int)valueIndex
{
return _enum->valueIndex();
}
- (void)setValueIndex:(int)valueIndex
{
_enum->valueIndex(valueIndex);
}
- (NSArray<NSString*>*)values
{
auto values = _enum->values();
NSMutableArray* mapped = [NSMutableArray arrayWithCapacity:values.size()];
for (auto it = values.begin(); it != values.end(); ++it)
{
auto value = *it;
NSString* string = [NSString stringWithCString:value.c_str()
encoding:NSUTF8StringEncoding];
[mapped addObject:string];
}
return mapped;
}
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceEnumPropertyListener)listener
{
return [super addListener:listener];
}
- (void)handleListeners
{
for (RiveDataBindingViewModelInstanceEnumPropertyListener listener in self
.listeners.allValues)
{
listener(self.value);
}
}
@end
#pragma mark - Trigger
@implementation RiveDataBindingViewModelInstanceTriggerProperty
{
rive::ViewModelInstanceTriggerRuntime* _trigger;
}
- (instancetype)initWithTrigger:(rive::ViewModelInstanceTriggerRuntime*)trigger
{
if (self = [super initWithValue:trigger])
{
_trigger = trigger;
}
return self;
}
- (void)dealloc
{
_trigger = nullptr;
}
- (void)trigger
{
_trigger->trigger();
[RiveLogger logPropertyTriggered:self];
}
- (NSUUID*)addListener:
(RiveDataBindingViewModelInstanceTriggerPropertyListener)listener
{
return [super addListener:listener];
}
- (void)handleListeners
{
for (RiveDataBindingViewModelInstanceTriggerPropertyListener listener in
self.listeners.allValues)
{
listener();
}
}
@end

View File

@@ -0,0 +1,38 @@
//
// RivePropertyData.h
// RiveRuntime
//
// Created by David Skuza on 2/4/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, RiveDataBindingViewModelInstancePropertyDataType) {
RiveDataBindingViewModelInstancePropertyDataTypeNone = 0,
RiveDataBindingViewModelInstancePropertyDataTypeString,
RiveDataBindingViewModelInstancePropertyDataTypeNumber,
RiveDataBindingViewModelInstancePropertyDataTypeBoolean,
RiveDataBindingViewModelInstancePropertyDataTypeColor,
RiveDataBindingViewModelInstancePropertyDataTypeList,
RiveDataBindingViewModelInstancePropertyDataTypeEnum,
RiveDataBindingViewModelInstancePropertyDataTypeTrigger,
RiveDataBindingViewModelInstancePropertyDataTypeViewModel,
} NS_SWIFT_NAME(RiveDataBindingViewModelInstancePropertyData.DataType);
NS_ASSUME_NONNULL_BEGIN
/// An object that represents the metadata of a view model instance property.
NS_SWIFT_NAME(RiveDataBindingViewModelInstanceProperty.Data)
@interface RiveDataBindingViewModelInstancePropertyData : NSObject
/// The type of property within the view model instance.
@property(nonatomic, readonly)
RiveDataBindingViewModelInstancePropertyDataType type;
/// The name of the property within the view model instance.
@property(nonatomic, readonly) NSString* name;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,52 @@
//
// RivePropertyData.m
// RiveRuntime
//
// Created by David Skuza on 2/4/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import <Rive.h>
#import <RivePrivateHeaders.h>
RiveDataBindingViewModelInstancePropertyDataType
RiveDataBindingViewModelInstancePropertyDataTypeFromRuntime(rive::DataType type)
{
switch (type)
{
case rive::DataType::none:
return RiveDataBindingViewModelInstancePropertyDataTypeNone;
case rive::DataType::string:
return RiveDataBindingViewModelInstancePropertyDataTypeString;
case rive::DataType::number:
return RiveDataBindingViewModelInstancePropertyDataTypeNumber;
case rive::DataType::boolean:
return RiveDataBindingViewModelInstancePropertyDataTypeBoolean;
case rive::DataType::color:
return RiveDataBindingViewModelInstancePropertyDataTypeColor;
case rive::DataType::list:
return RiveDataBindingViewModelInstancePropertyDataTypeList;
case rive::DataType::enumType:
return RiveDataBindingViewModelInstancePropertyDataTypeEnum;
case rive::DataType::trigger:
return RiveDataBindingViewModelInstancePropertyDataTypeTrigger;
case rive::DataType::viewModel:
return RiveDataBindingViewModelInstancePropertyDataTypeViewModel;
}
}
@implementation RiveDataBindingViewModelInstancePropertyData
- (instancetype)initWithData:(rive::PropertyData)data
{
if (self = [super init])
{
_type = RiveDataBindingViewModelInstancePropertyDataTypeFromRuntime(
data.type);
_name = [NSString stringWithCString:data.name.c_str()
encoding:NSUTF8StringEncoding];
}
return self;
}
@end

View File

@@ -0,0 +1,19 @@
//
// WeakContainer.h
// RiveRuntime
//
// Created by David Skuza on 3/13/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface WeakContainer<WeakType> : NSObject
@property(nonatomic, nullable, weak) WeakType object;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,13 @@
//
// WeakContainer.m
// RiveRuntime
//
// Created by David Skuza on 3/13/25.
// Copyright © 2025 Rive. All rights reserved.
//
#import "WeakContainer.h"
@implementation WeakContainer
@end

View File

@@ -12,6 +12,7 @@ import OSLog
enum RiveLoggerArtboardEvent {
case advance(Double)
case error(String)
case instanceBind(String)
}
extension RiveLogger {
@@ -25,6 +26,10 @@ extension RiveLogger {
log(artboard: artboard, event: .error(error))
}
@objc(logArtboard:instanceBind:) static func log(artboard: RiveArtboard, instanceBind name: String) {
log(artboard: artboard, event: .instanceBind(name))
}
static func log(artboard: RiveArtboard, event: RiveLoggerArtboardEvent) {
switch event {
case .advance(let elapsed):
@@ -36,6 +41,10 @@ extension RiveLogger {
_log(event: event, level: .error) {
Self.artboard.error("\(error)")
}
case .instanceBind(let name):
_log(event: event, level: .debug) {
Self.artboard.debug("\(self.prefix(for: artboard))Bound view model instance \(name)")
}
}
}

View File

@@ -0,0 +1,205 @@
//
// RiveLogger+DataBinding.swift
// RiveRuntime
//
// Created by David Skuza on 2/10/25.
// Copyright © 2025 Rive. All rights reserved.
//
import Foundation
import OSLog
private protocol DataBindingEvent { }
enum RiveLoggerDataBindingEvent {
enum ViewModel: DataBindingEvent {
case createdInstanceFromIndex(Int, Bool)
case createdInstanceFromName(String, Bool)
case createdDefaultInstance(Bool)
case createdInstance(Bool)
}
enum Instance: DataBindingEvent {
case property(String, Bool)
case stringProperty(String, Bool)
case numberProperty(String, Bool)
case booleanProperty(String, Bool)
case colorProperty(String, Bool)
case enumProperty(String, Bool)
case viewModelProperty(String, Bool)
case triggerProperty(String, Bool)
}
enum Property: DataBindingEvent {
case propertyUpdated(String, String)
case propertyTriggered(String)
}
}
extension RiveLogger {
private static let dataBinding = Logger(subsystem: subsystem, category: "rive-data-binding")
// MARK: - Log
static func log(viewModelRuntime viewModel: RiveDataBindingViewModel, event: RiveLoggerDataBindingEvent.ViewModel) {
switch event {
case .createdInstanceFromIndex(let index, let created):
_log(event: event, level: .debug) {
let start = created ? "Created" : "Could not create"
dataBinding.debug("[\(viewModel.name)] \(start) instance from index \(index)")
}
case .createdInstanceFromName(let name, let created):
_log(event: event, level: .debug) {
let start = created ? "Created" : "Could not create"
dataBinding.debug("[\(viewModel.name)] \(start) instance from name \(name)")
}
case .createdDefaultInstance(let created):
_log(event: event, level: .debug) {
let start = created ? "Created" : "Could not create"
dataBinding.debug("[\(viewModel.name)] \(start) default instance")
}
case .createdInstance(let created):
_log(event: event, level: .debug) {
let start = created ? "Created" : "Could not create"
dataBinding.debug("[\(viewModel.name)] \(start) new instance")
}
}
}
static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, event: RiveLoggerDataBindingEvent.Instance) {
switch event {
case .property(let path, let found):
_log(event: event, level: .debug) {
let message = message(instance: instance, for: "base", path: path, found: found)
dataBinding.debug("\(message)")
}
case .stringProperty(let path, let found):
_log(event: event, level: .debug) {
let message = message(instance: instance, for: "string", path: path, found: found)
dataBinding.debug("\(message)")
}
case .numberProperty(let path, let found):
_log(event: event, level: .debug) {
let message = message(instance: instance, for: "number", path: path, found: found)
dataBinding.debug("\(message)")
}
case .booleanProperty(let path, let found):
_log(event: event, level: .debug) {
let message = message(instance: instance, for: "boolean", path: path, found: found)
dataBinding.debug("\(message)")
}
case .colorProperty(let path, let found):
_log(event: event, level: .debug) {
let message = message(instance: instance, for: "color", path: path, found: found)
dataBinding.debug("\(message)")
}
case .enumProperty(let path, let found):
_log(event: event, level: .debug) {
let message = message(instance: instance, for: "enum", path: path, found: found)
dataBinding.debug("\(message)")
}
case .viewModelProperty(let path, let found):
_log(event: event, level: .debug) {
let message = message(instance: instance, for: "view model", path: path, found: found)
dataBinding.debug("\(message)")
}
case .triggerProperty(let path, let found):
_log(event: event, level: .debug) {
let message = message(instance: instance, for: "trigger", path: path, found: found)
dataBinding.debug("\(message)")
}
}
}
static func log(event: RiveLoggerDataBindingEvent.Property) {
switch event {
case .propertyUpdated(let name, let value):
_log(event: event, level: .debug) {
dataBinding.debug("[\(name)] Updated property value to \(value)")
}
case .propertyTriggered(let name):
_log(event: event, level: .debug) {
dataBinding.debug("[\(name)] Triggered")
}
}
}
// MARK: - RiveDataBindingViewModel
@objc static func log(viewModelRuntime runtime: RiveDataBindingViewModel, createdInstanceFromIndex index: Int, created: Bool) {
Self.log(viewModelRuntime: runtime, event: .createdInstanceFromIndex(index, created))
}
@objc static func log(viewModelRuntime runtime: RiveDataBindingViewModel, createdInstanceFromName name: String, created: Bool) {
Self.log(viewModelRuntime: runtime, event: .createdInstanceFromName(name, created))
}
@objc static func logViewModelRuntimeCreatedDefaultInstance(_ runtime: RiveDataBindingViewModel, created: Bool) {
Self.log(viewModelRuntime: runtime, event: .createdDefaultInstance(created))
}
@objc static func logViewModelRuntimeCreatedInstance(_ runtime: RiveDataBindingViewModel, created: Bool) {
Self.log(viewModelRuntime: runtime, event: .createdInstance(created))
}
// MARK: - RiveDataBindingViewModelInstance
@objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, propertyAtPath path: String, found: Bool) {
Self.log(viewModelInstance: instance, event: .property(path, found))
}
@objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, stringPropertyAtPath path: String, found: Bool) {
Self.log(viewModelInstance: instance, event: .stringProperty(path, found))
}
@objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, numberPropertyAtPath path: String, found: Bool) {
Self.log(viewModelInstance: instance, event: .numberProperty(path, found))
}
@objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, booleanPropertyAtPath path: String, found: Bool) {
Self.log(viewModelInstance: instance, event: .booleanProperty(path, found))
}
@objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, colorPropertyAtPath path: String, found: Bool) {
Self.log(viewModelInstance: instance, event: .colorProperty(path, found))
}
@objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, enumPropertyAtPath path: String, found: Bool) {
Self.log(viewModelInstance: instance, event: .enumProperty(path, found))
}
@objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, viewModelPropertyAtPath path: String, found: Bool) {
Self.log(viewModelInstance: instance, event: .viewModelProperty(path, found))
}
@objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, triggerPropertyAtPath path: String, found: Bool) {
Self.log(viewModelInstance: instance, event: .triggerProperty(path, found))
}
// MARK: - Properties
@objc(logPropertyUpdated:value:) static func log(propertyUpdated property: RiveDataBindingViewModel.Instance.Property, value: String) {
Self.log(event: .propertyUpdated(property.name, value))
}
@objc(logPropertyTriggered:) static func log(propertyTriggered property: RiveDataBindingViewModel.Instance.Property) {
Self.log(event: .propertyTriggered(property.name))
}
// MARK: - Private
private static func _log(event: DataBindingEvent, level: RiveLogLevel, log: () -> Void) {
guard isEnabled,
categories.contains(.dataBinding),
levels.contains(level)
else { return }
log()
}
private static func message(instance: RiveDataBindingViewModel.Instance, for type: String, path: String, found: Bool) -> String {
if found {
return "[\(instance.name)] Found \(type) property at path \(path)"
} else {
return "[\(instance.name)] Could not find \(type) property at path \(path)"
}
}
}

View File

@@ -18,6 +18,9 @@ enum RiveLoggerFileEvent {
case loadedAsset(RiveFileAsset)
case loadedFromURL(URL)
case loadingFromResource(String)
case viewModelWithName(String, Bool)
case viewModelAtIndex(Int, Bool)
case defaultViewModelForArtboard(String, Bool)
}
extension RiveLogger {
@@ -51,6 +54,18 @@ extension RiveLogger {
log(file: nil, event: .loadingFromResource(name))
}
@objc(logFileViewModelWithName:found:) static func log(fileViewModelName name: String, found: Bool) {
log(file: nil, event: .viewModelWithName(name, found))
}
@objc(logFileViewModelAtIndex:found:) static func log(fileViewModelAtIndex index: Int, found: Bool) {
log(file: nil, event: .viewModelAtIndex(index, found))
}
@objc(logFileDefaultViewModelForArtboard:found:) static func log(fileDefaultViewModelForArtboard artboard: RiveArtboard, found: Bool) {
log(file: nil, event: .defaultViewModelForArtboard(artboard.name(), found))
}
static func log(file: RiveFile?, event: RiveLoggerFileEvent) {
switch event {
case .fatalError(let message):
@@ -85,6 +100,21 @@ extension RiveLogger {
_log(event: event, level: .debug) {
Self.file.debug("Loading resource \(name)")
}
case .viewModelWithName(let name, let found):
_log(event: event, level: .debug) {
let message = found ? "Found view model named \(name)" : "Could not find view model named \(name)"
Self.file.debug("\(message)")
}
case .viewModelAtIndex(let index, let found):
_log(event: event, level: .debug) {
let message = found ? "Found view model at index \(index)" : "Could not find view model at index \(index)"
Self.file.debug("\(message)")
}
case .defaultViewModelForArtboard(let name, let found):
_log(event: event, level: .debug) {
let message = found ? "Found default view model for artboard \(name)" : "Could not find default view for artboard \(name)"
Self.file.debug("\(message)")
}
}
}

View File

@@ -13,6 +13,7 @@ enum RiveLoggerStateMachineEvent {
case advance(Double)
case eventReceived(RiveEvent)
case error(String)
case instanceBind(String)
}
extension RiveLogger {
@@ -26,6 +27,10 @@ extension RiveLogger {
log(stateMachine: stateMachine, event: .error(error))
}
@objc(logStateMachine:instanceBind:) static func log(stateMachine: RiveStateMachineInstance, instanceBind name: String) {
log(stateMachine: stateMachine, event: .instanceBind(name))
}
static func log(stateMachine: RiveStateMachineInstance, event: RiveLoggerStateMachineEvent) {
switch event {
case .advance(let elapsed):
@@ -41,6 +46,10 @@ extension RiveLogger {
_log(event: event, level: .error) {
Self.stateMachine.error("\(error)")
}
case .instanceBind(let name):
_log(event: event, level: .debug) {
Self.stateMachine.debug("\(self.prefix(for: stateMachine))Bound view model instance \(name)")
}
}
}

View File

@@ -66,11 +66,13 @@ import OSLog
@objc public static let file = RiveLogCategory(rawValue: 1 << 4)
/// The category used when logging from a Rive view.
@objc public static let view = RiveLogCategory(rawValue: 1 << 5)
/// The category used when logging Data Binding.
@objc public static let dataBinding = RiveLogCategory(rawValue: 1 << 6)
/// An option set of no categories.
@objc public static let none: RiveLogCategory = []
/// An option set containing all possible categories
@objc public static let all: RiveLogCategory = [.stateMachine, .artboard, .viewModel, .model, .file, .view]
@objc public static let all: RiveLogCategory = [.stateMachine, .artboard, .viewModel, .model, .file, .view, .dataBinding]
override public func isEqual(_ object: Any?) -> Bool {
guard let other = object as? RiveLogCategory else { return false }

View File

@@ -420,4 +420,25 @@ static int artInstanceCount = 0;
_artboardInstance->height(_artboardInstance->originalHeight());
}
#pragma mark - Data Binding
- (void)bindViewModelInstance:(RiveDataBindingViewModelInstance*)instance
{
// Let's walk through the instances of the word instance
//
// _artboardInstance is the underlying c++ type of ourself
// to which we bind
//
// instance is the ObjC bridging type of the underlying
// c++ type of a view model instance.
//
// instance.instance is the underlying c++ type of the bridging type
// so that we can call into the c++ runtime
//
// instance.instance->instance() is the c++ rcp of the actual
// type that gets bound to the artboard
_artboardInstance->bindViewModelInstance(instance.instance->instance());
[RiveLogger logArtboard:self instanceBind:instance.name];
}
@end

View File

@@ -494,6 +494,50 @@
return artboardNames;
}
#pragma mark - Data Binding
- (NSUInteger)viewModelCount
{
return riveFile->viewModelCount();
}
- (nullable id)viewModelAtIndex:(NSUInteger)index
{
auto viewModel = riveFile->viewModelByIndex(index);
if (viewModel == nullptr)
{
[RiveLogger logFileViewModelAtIndex:index found:NO];
return nil;
}
[RiveLogger logFileViewModelAtIndex:index found:YES];
return [[RiveDataBindingViewModel alloc] initWithViewModel:viewModel];
}
- (nullable id)viewModelNamed:(NSString*)name
{
auto viewModel = riveFile->viewModelByName(std::string([name UTF8String]));
if (viewModel == nullptr)
{
[RiveLogger logFileViewModelWithName:name found:NO];
return nil;
}
[RiveLogger logFileViewModelWithName:name found:YES];
return [[RiveDataBindingViewModel alloc] initWithViewModel:viewModel];
}
- (RiveDataBindingViewModel*)defaultViewModelForArtboard:(RiveArtboard*)artboard
{
auto viewModel =
riveFile->defaultArtboardViewModel(artboard.artboardInstance);
if (viewModel == nullptr)
{
[RiveLogger logFileDefaultViewModelForArtboard:artboard found:NO];
return nil;
}
[RiveLogger logFileDefaultViewModelForArtboard:artboard found:YES];
return [[RiveDataBindingViewModel alloc] initWithViewModel:viewModel];
}
/// Clean up rive file
- (void)dealloc
{

View File

@@ -430,4 +430,28 @@ RiveHitResult RiveHitResultFromRuntime(rive::HitResult result)
instance->pointerUp(rive::Vec2D(touchLocation.x, touchLocation.y)));
}
#pragma mark - Data Binding
// Argument named i to not conflict with higher-level private variable named
// instance
- (void)bindViewModelInstance:(RiveDataBindingViewModelInstance*)i
{
// Let's walk through the instances of the word instance
//
// instance is the underlying c++ type of ourself
// to which we bind
//
// i is the ObjC bridging type of the underlying
// c++ type of a view model instance.
//
// i.instance is the underlying c++ type of the bridging type
// so that we can call into the c++ runtime
//
// i.instance->instance() is the c++ rcp of the actual
// type that gets bound to the state machine
instance->bindViewModelInstance(i.instance->instance());
_viewModelInstance = i;
[RiveLogger logStateMachine:self instanceBind:i.name];
}
@end

View File

@@ -28,6 +28,11 @@
#import <RiveRuntime/CDNFileAssetLoader.h>
#import <RiveRuntime/RiveFont.h>
#import <RiveRuntime/RiveDataBindingViewModel.h>
#import <RiveRuntime/RiveDataBindingViewModelInstance.h>
#import <RiveRuntime/RiveDataBindingViewModelInstanceProperty.h>
#import <RiveRuntime/RiveDataBindingViewModelInstancePropertyData.h>
NS_ASSUME_NONNULL_BEGIN
/*

View File

@@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
@class RiveStateMachineInstance;
@class RiveRenderer;
@class RiveTextValueRun;
@class RiveDataBindingViewModelInstance;
// MARK: - RiveArtboard
//
@@ -59,6 +60,19 @@ NS_ASSUME_NONNULL_BEGIN
- (void)advanceBy:(double)elapsedSeconds;
- (void)draw:(RiveRenderer*)renderer;
// MARK: - Data Binding
/// Binds an instance of a view model to the artboard for updates.
///
/// A strong reference to the instance being bound must be made if you wish to
/// reuse instance properties, or for observability.
///
/// The same instance must also be bound to a state machine, if one exists.
///
/// - Parameter instance: The instance of a view model to bind.
- (void)bindViewModelInstance:(RiveDataBindingViewModelInstance*)instance
NS_SWIFT_NAME(bind(viewModelInstance:));
// MARK: Debug
#if RIVE_ENABLE_REFERENCE_COUNTING

View File

@@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
@protocol RiveFileDelegate;
@class RiveFileAsset;
@class RiveFactory;
@class RiveDataBindingViewModel;
typedef bool (^LoadAsset)(RiveFileAsset* asset,
NSData* data,
RiveFactory* factory);
@@ -35,6 +36,9 @@ typedef bool (^LoadAsset)(RiveFileAsset* asset,
/// Delegate for calling when a file has finished loading
@property(weak) id delegate;
/// The number of view models in the file.
@property(nonatomic, readonly) NSUInteger viewModelCount;
/// Used to manage url sessions Rive, this is to enable testing.
- (nullable instancetype)initWithByteArray:(NSArray*)bytes
loadCdn:(bool)cdn
@@ -105,6 +109,49 @@ typedef bool (^LoadAsset)(RiveFileAsset* asset,
/// Returns the names of all artboards in the file.
- (NSArray<NSString*>*)artboardNames;
#pragma mark - Data Binding
/// Returns a view model from the file by index.
///
/// The index of a view model starts at 0, where 0 is the first view model
/// listed in the editor's "Data" panel from top-to-bottom.
///
/// Unlike `RiveDataBindingViewModel.Instance`, a strong reference to this model
/// does not have to be made.
///
/// - Parameter index: The index of the view model.
///
/// - Returns: A view model if one exists by index, otherwise nil.
- (nullable RiveDataBindingViewModel*)viewModelAtIndex:(NSUInteger)index;
/// Returns a view model from the file by name.
///
/// The name of the view model has to match the name of a view model in the
/// editor's "Data" panel.
///
/// Unlike `RiveDataBindingViewModel.Instance`, a strong reference to this model
/// does not have to be made.
///
/// - Parameter name: The name of the view model.
///
/// - Returns: A view model if one exists by name, otherwise nil.
- (nullable RiveDataBindingViewModel*)viewModelNamed:(nonnull NSString*)name;
/// Returns the default view model for an artboard.
///
/// The default view model is the view model selected under the "Data Bind"
/// panel for an artboard.
///
/// Unlike `RiveDataBindingViewModel.Instance`, a strong reference to this model
/// does not have to be made.
///
/// - Parameter artboard: The artboard within the `RiveFile` that contains a
/// data binding view model.
///
/// - Returns: A view model if one exists for the artboard, otherwise nil.
- (nullable RiveDataBindingViewModel*)defaultViewModelForArtboard:
(RiveArtboard*)artboard;
@end
/*

View File

@@ -33,6 +33,8 @@
#import "rive/assets/audio_asset.hpp"
#import "rive/assets/file_asset.hpp"
#import "rive/file_asset_loader.hpp"
#import "rive/viewmodel/runtime/viewmodel_instance_runtime.hpp"
#import "rive/viewmodel/runtime/viewmodel_runtime.hpp"
#include "rive/open_url_event.hpp"
#include "rive/custom_property_boolean.hpp"
@@ -43,6 +45,8 @@
#define RIVE_ENABLE_REFERENCE_COUNTING false
NS_ASSUME_NONNULL_BEGIN
// MARK: - Public Interfaces
/*
@@ -170,3 +174,62 @@
- (instancetype)initWithAudio:(rive::rcp<rive::AudioSource>)audio;
- (rive::rcp<rive::AudioSource>)instance;
@end
@interface RiveDataBindingViewModel ()
- (instancetype)initWithViewModel:(rive::ViewModelRuntime*)viewModel;
@end
@protocol RiveDataBindingViewModelInstancePropertyDelegate
- (void)valuePropertyDidAddListener:
(RiveDataBindingViewModelInstanceProperty*)value;
- (void)valuePropertyDidRemoveListener:
(RiveDataBindingViewModelInstanceProperty*)listener
isEmpty:(BOOL)isEmpty;
@end
@interface RiveDataBindingViewModelInstance ()
@property(nonatomic, readonly) rive::ViewModelInstanceRuntime* instance;
- (instancetype)initWithInstance:(rive::ViewModelInstanceRuntime*)instance;
- (void)cacheProperty:(RiveDataBindingViewModelInstanceProperty*)value
withPath:(NSString*)path;
@end
@interface RiveDataBindingViewModelInstanceProperty ()
@property(nonatomic, weak) id<RiveDataBindingViewModelInstancePropertyDelegate>
valueDelegate;
@property(nonatomic, readonly) NSDictionary<NSUUID*, id>* listeners;
- (instancetype)initWithValue:(rive::ViewModelInstanceValueRuntime*)value;
- (NSUUID*)addListener:(id)listener;
- (void)removeListener:(NSUUID*)listener;
- (void)handleListeners;
@end
@interface RiveDataBindingViewModelInstanceStringProperty ()
- (instancetype)initWithString:(rive::ViewModelInstanceStringRuntime*)string;
@end
@interface RiveDataBindingViewModelInstanceNumberProperty ()
- (instancetype)initWithNumber:(rive::ViewModelInstanceNumberRuntime*)number;
@end
@interface RiveDataBindingViewModelInstanceBooleanProperty ()
- (instancetype)initWithBoolean:(rive::ViewModelInstanceBooleanRuntime*)boolean;
@end
@interface RiveDataBindingViewModelInstanceColorProperty ()
- (instancetype)initWithColor:(rive::ViewModelInstanceColorRuntime*)color;
@end
@interface RiveDataBindingViewModelInstanceEnumProperty ()
- (instancetype)initWithEnum:(rive::ViewModelInstanceEnumRuntime*)e;
@end
@interface RiveDataBindingViewModelInstanceTriggerProperty ()
- (instancetype)initWithTrigger:(rive::ViewModelInstanceTriggerRuntime*)trigger;
@end
@interface RiveDataBindingViewModelInstancePropertyData ()
- (instancetype)initWithData:(rive::PropertyData)data;
@end
NS_ASSUME_NONNULL_END

View File

@@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
@class RiveSMINumber;
@class RiveLayerState;
@class RiveEvent;
@class RiveDataBindingViewModelInstance;
/// A type mirroring rive::HitResult, but available in both ObjC and Swift.
typedef NS_ENUM(NSInteger, RiveHitResult) { none, hit, hitOpaque };
@@ -28,6 +29,10 @@ typedef NS_ENUM(NSInteger, RiveHitResult) { none, hit, hitOpaque };
* RiveStateMachineInstance
*/
@interface RiveStateMachineInstance : NSObject
@property(nonatomic, nullable, readonly)
RiveDataBindingViewModelInstance* viewModelInstance;
- (NSString*)name;
- (bool)advanceBy:(double)elapsedSeconds;
- (const RiveSMIBool*)getBool:(NSString*)name;
@@ -76,6 +81,19 @@ typedef NS_ENUM(NSInteger, RiveHitResult) { none, hit, hitOpaque };
/// location.
- (RiveHitResult)touchCancelledAtLocation:(CGPoint)touchLocation;
// MARK: - Data Binding
/// Binds an instance of a view model to the state machine for updates.
///
/// A strong reference to the instance being bound must be made if you wish to
/// reuse instance properties, or for observability. By default, the instance
/// will also automatically be bound to the artboard containing the state
/// machine.
///
/// - Parameter instance: The instance of a view model to bind.
- (void)bindViewModelInstance:(RiveDataBindingViewModelInstance*)instance
NS_SWIFT_NAME(bind(viewModelInstance:));
// MARK: Debug
#if RIVE_ENABLE_REFERENCE_COUNTING

View File

@@ -10,12 +10,17 @@ import Foundation
import Combine
@objc open class RiveModel: NSObject, ObservableObject {
public typealias AutoBindCallback = (RiveDataBindingViewModel.Instance) -> Void
// NOTE: the order here determines the order in which memory garbage collected
public internal(set) var stateMachine: RiveStateMachineInstance?
public internal(set) var animation: RiveLinearAnimationInstance?
public private(set) var artboard: RiveArtboard!
internal private(set) var riveFile: RiveFile
public private(set) var riveFile: RiveFile
private var isAutoBindEnabled = false
private var autoBindCallback: AutoBindCallback?
public init(riveFile: RiveFile) {
self.riveFile = riveFile
}
@@ -59,6 +64,7 @@ import Combine
animation = nil
artboard = try riveFile.artboard(fromName: name)
artboard.__volume = _volume
autoBind()
}
catch { throw RiveModelError.invalidArtboard("Name \(name) not found") }
}
@@ -72,6 +78,7 @@ import Combine
artboard = try riveFile.artboard(from: index)
artboard.__volume = _volume
RiveLogger.log(model: self, event: .artboardByIndex(index))
autoBind()
}
catch {
let errorMessage = "Artboard at index \(index) not found"
@@ -84,6 +91,7 @@ import Combine
artboard = try riveFile.artboard()
artboard.__volume = _volume
RiveLogger.log(model: self, event: .defaultArtboard)
autoBind()
}
catch {
let errorMessage = "No Default Artboard"
@@ -97,6 +105,7 @@ import Combine
do {
stateMachine = try artboard.stateMachine(fromName: name)
RiveLogger.log(model: self, event: .stateMachineByName(name))
autoBind()
}
catch {
let errorMessage = "State machine named \(name) not found"
@@ -124,6 +133,8 @@ import Combine
stateMachine = try artboard.stateMachine(from: 0)
RiveLogger.log(model: self, event: .stateMachineByIndex(0))
}
autoBind()
}
catch {
let errorMessage = "State machine at index \(index ?? 0) not found"
@@ -158,7 +169,43 @@ import Combine
throw RiveModelError.invalidAnimation(errorMessage)
}
}
// MARK: - Data Binding
/// Automatically binds the default instance of the current artboard when the artboard and/or state machine changes,
/// including when it is first set. The callback will be called with the instance that has been bound.
/// A strong reference to the instance must be made in order to update properties and utilize observability.
///
/// - Parameter callback: The callback to be called when a `RiveDataBindingViewModel.Instance`
/// is bound to the current artboard and/or state machine.
@objc open func enableAutoBind(_ callback: @escaping AutoBindCallback) {
isAutoBindEnabled = true
autoBindCallback = callback
autoBind()
}
/// Disables the auto-binding featured enabled by `enableAutoBind`.
@objc open func disableAutoBind() {
isAutoBindEnabled = false
autoBindCallback = nil
}
private func autoBind() {
// autobind needs at _least_ an artboard
guard isAutoBindEnabled,
let artboard,
let viewModel = riveFile.defaultViewModel(for: artboard),
let instance = viewModel.createDefaultInstance()
else { return }
artboard.bind(viewModelInstance: instance)
// If, for some reason, there is no state machine (e.g linear animation) then no need to bind
stateMachine?.bind(viewModelInstance: instance)
autoBindCallback?(instance)
}
// MARK: -
public override var description: String {

View File

@@ -376,6 +376,8 @@ open class RiveView: RiveRendererView {
if let delegate = stateMachineDelegate {
stateMachine.stateChanges().forEach { delegate.stateMachine?(stateMachine, didChangeState: $0) }
}
stateMachine.viewModelInstance?.updateListeners()
} else if let animation = riveModel?.animation {
isPlaying = animation.advance(by: delta) && wasPlaying