mirror of
https://github.com/rive-app/rive-ios.git
synced 2026-01-18 17:11:28 +01:00
feat(apple): add support for data binding list properties (#9936) d2997eeef4
Co-authored-by: David Skuza <david@rive.app>
This commit is contained in:
@@ -1 +1 @@
|
||||
7379bdd49fc2b6c2a209b447b1d7d74e730f41df
|
||||
d2997eeef48f74a50e6eda2d5691e295ad328f51
|
||||
|
||||
@@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@class RiveDataBindingViewModelInstanceEnumProperty;
|
||||
@class RiveDataBindingViewModelInstanceTriggerProperty;
|
||||
@class RiveDataBindingViewModelInstanceImageProperty;
|
||||
@class RiveDataBindingViewModelInstanceListProperty;
|
||||
@class RiveDataBindingViewModelInstancePropertyData;
|
||||
|
||||
/// An object that represents an instance of a view model, used to update
|
||||
@@ -87,7 +88,7 @@ NS_SWIFT_NAME(RiveDataBindingViewModel.Instance)
|
||||
/// 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.
|
||||
/// - Parameter path: The path to the boolean property.
|
||||
///
|
||||
/// - Returns: The property if it exists at the supplied path, otherwise nil.
|
||||
- (nullable RiveDataBindingViewModelInstanceBooleanProperty*)
|
||||
@@ -100,7 +101,7 @@ NS_SWIFT_NAME(RiveDataBindingViewModel.Instance)
|
||||
/// 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.
|
||||
/// - Parameter path: The path to the color property.
|
||||
///
|
||||
/// - Returns: The property if it exists at the supplied path, otherwise nil.
|
||||
- (nullable RiveDataBindingViewModelInstanceColorProperty*)
|
||||
@@ -113,7 +114,7 @@ NS_SWIFT_NAME(RiveDataBindingViewModel.Instance)
|
||||
/// 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.
|
||||
/// - Parameter path: The path to the enum property.
|
||||
///
|
||||
/// - Returns: The property if it exists at the supplied path, otherwise nil.
|
||||
- (nullable RiveDataBindingViewModelInstanceEnumProperty*)enumPropertyFromPath:
|
||||
@@ -126,7 +127,7 @@ NS_SWIFT_NAME(RiveDataBindingViewModel.Instance)
|
||||
/// 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.
|
||||
/// - Parameter path: The path to the view model instance property.
|
||||
///
|
||||
/// - Returns: The property if it exists at the supplied path, otherwise nil.
|
||||
- (nullable RiveDataBindingViewModelInstance*)viewModelInstancePropertyFromPath:
|
||||
@@ -154,7 +155,7 @@ NS_SWIFT_NAME(RiveDataBindingViewModel.Instance)
|
||||
/// 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.
|
||||
/// - Parameter path: The path to the trigger property.
|
||||
///
|
||||
/// - Returns: The property if it exists at the supplied path, otherwise nil.
|
||||
- (nullable RiveDataBindingViewModelInstanceTriggerProperty*)
|
||||
@@ -167,12 +168,25 @@ NS_SWIFT_NAME(RiveDataBindingViewModel.Instance)
|
||||
/// 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.
|
||||
/// - Parameter path: The path to the image property.
|
||||
///
|
||||
/// - Returns: The property if it exists at the supplied path, otherwise nil.
|
||||
- (nullable RiveDataBindingViewModelInstanceImageProperty*)
|
||||
imagePropertyFromPath:(NSString*)path;
|
||||
|
||||
/// Gets a list 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 list property.
|
||||
///
|
||||
/// - Returns: The property if it exists at the supplied path, otherwise nil.
|
||||
- (nullable RiveDataBindingViewModelInstanceListProperty*)listPropertyFromPath:
|
||||
(NSString*)path;
|
||||
|
||||
/// Calls all registered property listeners for the properties of the view model
|
||||
/// instance.
|
||||
- (void)updateListeners;
|
||||
|
||||
@@ -347,6 +347,41 @@
|
||||
return imageProperty;
|
||||
}
|
||||
|
||||
- (RiveDataBindingViewModelInstanceListProperty*)listPropertyFromPath:
|
||||
(NSString*)path
|
||||
{
|
||||
RiveDataBindingViewModelInstanceListProperty* cached;
|
||||
if ((cached = [self
|
||||
cachedPropertyFromPath:path
|
||||
asClass:
|
||||
[RiveDataBindingViewModelInstanceListProperty
|
||||
class]]))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
auto list = _instance->propertyList(std::string([path UTF8String]));
|
||||
if (list == nullptr)
|
||||
{
|
||||
[RiveLogger logWithViewModelInstance:self
|
||||
listPropertyAtPath:path
|
||||
found:NO];
|
||||
return nil;
|
||||
}
|
||||
|
||||
[RiveLogger logWithViewModelInstance:self
|
||||
listPropertyAtPath:path
|
||||
found:YES];
|
||||
RiveDataBindingViewModelInstanceListProperty* listProperty =
|
||||
[[RiveDataBindingViewModelInstanceListProperty alloc]
|
||||
initWithList:list];
|
||||
listProperty.valueDelegate = self;
|
||||
|
||||
[self cacheProperty:listProperty withPath:path];
|
||||
|
||||
return listProperty;
|
||||
}
|
||||
|
||||
- (void)updateListeners
|
||||
{
|
||||
[_properties enumerateKeysAndObjectsUsingBlock:^(
|
||||
|
||||
@@ -274,7 +274,7 @@ NS_SWIFT_NAME(RiveDataBindingViewModelInstance.ImageProperty)
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (void)setValue:(nonnull RiveRenderImage*)image;
|
||||
- (void)setValue:(nullable RiveRenderImage*)image;
|
||||
|
||||
/// Adds a block as a listener, called when the property is triggered.
|
||||
///
|
||||
@@ -291,4 +291,78 @@ NS_SWIFT_NAME(RiveDataBindingViewModelInstance.ImageProperty)
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - List
|
||||
|
||||
typedef void (^RiveDataBindingViewModelInstanceListPropertyListener)(void)
|
||||
NS_SWIFT_NAME(RiveDataBindingViewModelInstanceListProperty.Listener);
|
||||
|
||||
/// An object that represents a trigger property of a view model instance.
|
||||
NS_SWIFT_NAME(RiveDataBindingViewModelInstance.ListProperty)
|
||||
@interface RiveDataBindingViewModelInstanceListProperty
|
||||
: RiveDataBindingViewModelInstanceProperty
|
||||
|
||||
/// The number of instances in the list.
|
||||
@property(nonatomic, readonly) NSUInteger count;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/// Gets an instance at the specified index.
|
||||
///
|
||||
/// - Parameter index: The index of the instance to get.
|
||||
///
|
||||
/// - Returns: The instance at the specified index, or nil if the index is out
|
||||
/// of range.
|
||||
- (nullable RiveDataBindingViewModelInstance*)instanceAtIndex:(int)index
|
||||
NS_SWIFT_NAME(instance(at:));
|
||||
|
||||
/// Adds an instance to the list.
|
||||
///
|
||||
/// - Parameter instance: The instance to add.
|
||||
- (void)addInstance:(RiveDataBindingViewModelInstance*)instance
|
||||
NS_SWIFT_NAME(append(_:));
|
||||
|
||||
/// Inserts an instance to the list at a given index.
|
||||
///
|
||||
/// - Parameter instance: The instance to add to the list.
|
||||
/// - Parameter index: The index in the list at which to insert an instance.
|
||||
/// This value must not be greater than the count of elements in the array.
|
||||
///
|
||||
/// - Returns: `true` if the instance has been added, otherwise `false`.
|
||||
- (BOOL)insertInstance:(RiveDataBindingViewModelInstance*)instance
|
||||
atIndex:(int)index NS_SWIFT_NAME(insert(_:at:));
|
||||
|
||||
/// Removes an instance from the list.
|
||||
///
|
||||
/// - Parameter instance: The instance to remove.
|
||||
- (void)removeInstance:(RiveDataBindingViewModelInstance*)instance
|
||||
NS_SWIFT_NAME(remove(_:));
|
||||
|
||||
/// Removes an instance at the specified index.
|
||||
///
|
||||
/// - Parameter index: The index of the instance to remove.
|
||||
- (void)removeInstanceAtIndex:(int)index NS_SWIFT_NAME(remove(at:));
|
||||
|
||||
/// Swaps two instances in the list.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - firstIndex: The index of the first instance.
|
||||
/// - secondIndex: The index of the second instance.
|
||||
- (void)swapInstanceAtIndex:(uint32_t)firstIndex
|
||||
withInstanceAtIndex:(uint32_t)secondIndex NS_SWIFT_NAME(swap(at:with:));
|
||||
|
||||
/// Adds a block as a listener, called when there is a change to the list.
|
||||
///
|
||||
/// - 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 there is a change
|
||||
/// to the list.
|
||||
///
|
||||
/// - Returns: A UUID for the listener, used in conjunction with
|
||||
/// `removeListener`.
|
||||
- (NSUUID*)addListener:
|
||||
(RiveDataBindingViewModelInstanceListPropertyListener)listener;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -555,6 +555,12 @@
|
||||
|
||||
- (void)setValue:(RiveRenderImage*)renderImage
|
||||
{
|
||||
if (renderImage == nil)
|
||||
{
|
||||
[RiveLogger logPropertyUpdated:self value:@"nil"];
|
||||
_image->value(nullptr);
|
||||
}
|
||||
|
||||
[RiveLogger logPropertyUpdated:self value:@"new image"];
|
||||
_image->value([renderImage instance].get());
|
||||
}
|
||||
@@ -575,3 +581,114 @@
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - List
|
||||
|
||||
@implementation RiveDataBindingViewModelInstanceListProperty
|
||||
{
|
||||
rive::ViewModelInstanceListRuntime* _list;
|
||||
NSMutableDictionary<NSValue*, RiveDataBindingViewModelInstance*>*
|
||||
_instances;
|
||||
}
|
||||
|
||||
- (instancetype)initWithList:(rive::ViewModelInstanceListRuntime*)list
|
||||
{
|
||||
if (self = [super initWithValue:list])
|
||||
{
|
||||
_list = list;
|
||||
_instances = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable RiveDataBindingViewModelInstance*)instanceAtIndex:(int)index
|
||||
{
|
||||
auto instance = _list->instanceAt(index);
|
||||
if (instance == nullptr)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSValue* key = [NSValue valueWithPointer:instance];
|
||||
RiveDataBindingViewModelInstance* cachedInstance = _instances[key];
|
||||
if (cachedInstance != nil)
|
||||
{
|
||||
return cachedInstance;
|
||||
}
|
||||
|
||||
RiveDataBindingViewModelInstance* newInstance =
|
||||
[[RiveDataBindingViewModelInstance alloc] initWithInstance:instance];
|
||||
_instances[key] = newInstance;
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
- (void)addInstance:(RiveDataBindingViewModelInstance*)instance
|
||||
{
|
||||
auto i = [instance instance];
|
||||
_list->addInstance(i);
|
||||
|
||||
NSValue* key = [NSValue valueWithPointer:i];
|
||||
_instances[key] = instance;
|
||||
}
|
||||
|
||||
- (BOOL)insertInstance:(RiveDataBindingViewModelInstance*)instance
|
||||
atIndex:(int)index
|
||||
{
|
||||
auto i = [instance instance];
|
||||
BOOL success = _list->addInstanceAt(i, index);
|
||||
if (success)
|
||||
{
|
||||
NSValue* key = [NSValue valueWithPointer:i];
|
||||
_instances[key] = instance;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (void)removeInstance:(RiveDataBindingViewModelInstance*)instance
|
||||
{
|
||||
auto i = [instance instance];
|
||||
_list->removeInstance(i);
|
||||
|
||||
NSValue* key = [NSValue valueWithPointer:i];
|
||||
[_instances removeObjectForKey:key];
|
||||
}
|
||||
|
||||
- (void)removeInstanceAtIndex:(int)index
|
||||
{
|
||||
auto i = _list->instanceAt(index);
|
||||
if (i != nullptr)
|
||||
{
|
||||
NSValue* key = [NSValue valueWithPointer:i];
|
||||
[_instances removeObjectForKey:key];
|
||||
}
|
||||
|
||||
_list->removeInstanceAt(index);
|
||||
}
|
||||
|
||||
- (void)swapInstanceAtIndex:(uint32_t)firstIndex
|
||||
withInstanceAtIndex:(uint32_t)secondIndex
|
||||
{
|
||||
_list->swap(firstIndex, secondIndex);
|
||||
}
|
||||
|
||||
- (NSUInteger)count
|
||||
{
|
||||
return _list->size();
|
||||
}
|
||||
|
||||
- (NSUUID*)addListener:
|
||||
(RiveDataBindingViewModelInstanceListPropertyListener)listener
|
||||
{
|
||||
return [super addListener:listener];
|
||||
}
|
||||
|
||||
- (void)handleListeners
|
||||
{
|
||||
for (RiveDataBindingViewModelInstanceListPropertyListener listener in self
|
||||
.listeners.allValues)
|
||||
{
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -28,6 +28,7 @@ enum RiveLoggerDataBindingEvent {
|
||||
case viewModelProperty(String, Bool)
|
||||
case triggerProperty(String, Bool)
|
||||
case imageProperty(String, Bool)
|
||||
case listProperty(String, Bool)
|
||||
}
|
||||
enum Property: DataBindingEvent {
|
||||
case propertyUpdated(String, String)
|
||||
@@ -112,6 +113,11 @@ extension RiveLogger {
|
||||
let message = message(instance: instance, for: "image", path: path, found: found)
|
||||
dataBinding.debug("\(message)")
|
||||
}
|
||||
case .listProperty(let path, let found):
|
||||
_log(event: event, level: .debug) {
|
||||
let message = message(instance: instance, for: "list", path: path, found: found)
|
||||
dataBinding.debug("\(message)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +190,10 @@ extension RiveLogger {
|
||||
Self.log(viewModelInstance: instance, event: .imageProperty(path, found))
|
||||
}
|
||||
|
||||
@objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, listPropertyAtPath path: String, found: Bool) {
|
||||
Self.log(viewModelInstance: instance, event: .listProperty(path, found))
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@objc(logPropertyUpdated:value:) static func log(propertyUpdated property: RiveDataBindingViewModel.Instance.Property, value: String) {
|
||||
|
||||
@@ -236,4 +236,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (instancetype)initWithImage:(rive::ViewModelInstanceAssetImageRuntime*)image;
|
||||
@end
|
||||
|
||||
@interface RiveDataBindingViewModelInstanceListProperty ()
|
||||
- (instancetype)initWithList:(rive::ViewModelInstanceListRuntime*)list;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
Binary file not shown.
@@ -83,51 +83,57 @@ class DataBindingTests: XCTestCase {
|
||||
|
||||
func test_viewModel_propertyCount_returnsCount() {
|
||||
let viewModel = file.viewModelNamed("Test")!
|
||||
XCTAssertEqual(viewModel.propertyCount, 10)
|
||||
XCTAssertEqual(viewModel.propertyCount, 12)
|
||||
}
|
||||
|
||||
func test_viewModel_properties_returnsAllProperties() {
|
||||
let viewModel = file.viewModelNamed("Test")!
|
||||
|
||||
XCTAssertEqual(viewModel.properties.count, 10)
|
||||
|
||||
var data = viewModel.properties[0]
|
||||
XCTAssertEqual(data.type, .list)
|
||||
XCTAssertEqual(data.name, "List")
|
||||
|
||||
data = viewModel.properties[1]
|
||||
XCTAssertEqual(data.type, .assetImage)
|
||||
XCTAssertEqual(data.name, "Image")
|
||||
|
||||
data = viewModel.properties[2]
|
||||
XCTAssertEqual(data.type, .viewModel)
|
||||
XCTAssertEqual(data.name, "SecondNested")
|
||||
|
||||
data = viewModel.properties[1]
|
||||
data = viewModel.properties[3]
|
||||
XCTAssertEqual(data.type, .trigger)
|
||||
XCTAssertEqual(data.name, "Trigger Blue")
|
||||
|
||||
data = viewModel.properties[2]
|
||||
data = viewModel.properties[4]
|
||||
XCTAssertEqual(data.type, .trigger)
|
||||
XCTAssertEqual(data.name, "Trigger Green")
|
||||
|
||||
data = viewModel.properties[3]
|
||||
data = viewModel.properties[5]
|
||||
XCTAssertEqual(data.type, .trigger)
|
||||
XCTAssertEqual(data.name, "Trigger Red")
|
||||
|
||||
data = viewModel.properties[4]
|
||||
data = viewModel.properties[6]
|
||||
XCTAssertEqual(data.type, .viewModel)
|
||||
XCTAssertEqual(data.name, "Nested")
|
||||
|
||||
data = viewModel.properties[5]
|
||||
data = viewModel.properties[7]
|
||||
XCTAssertEqual(data.type, .enum)
|
||||
XCTAssertEqual(data.name, "Enum")
|
||||
|
||||
data = viewModel.properties[6]
|
||||
data = viewModel.properties[8]
|
||||
XCTAssertEqual(data.type, .color)
|
||||
XCTAssertEqual(data.name, "Color")
|
||||
|
||||
data = viewModel.properties[7]
|
||||
data = viewModel.properties[9]
|
||||
XCTAssertEqual(data.type, .boolean)
|
||||
XCTAssertEqual(data.name, "Boolean")
|
||||
|
||||
data = viewModel.properties[8]
|
||||
data = viewModel.properties[10]
|
||||
XCTAssertEqual(data.type, .number)
|
||||
XCTAssertEqual(data.name, "Number")
|
||||
|
||||
data = viewModel.properties[9]
|
||||
data = viewModel.properties[11]
|
||||
XCTAssertEqual(data.type, .string)
|
||||
XCTAssertEqual(data.name, "String")
|
||||
}
|
||||
@@ -462,6 +468,61 @@ class DataBindingTests: XCTestCase {
|
||||
let renderImage = RiveRenderImage(data: data)!
|
||||
property.setValue(renderImage)
|
||||
XCTAssertTrue(property.hasChanged)
|
||||
property.clearChanges()
|
||||
property.setValue(nil)
|
||||
XCTAssertTrue(property.hasChanged)
|
||||
}
|
||||
|
||||
// MARK: Lists
|
||||
|
||||
func test_viewModelInstance_listProperty_canAddAndRemoveAndSwapIdenticalItems() throws {
|
||||
let instance = file.viewModelNamed("Test")!.createDefaultInstance()!
|
||||
|
||||
let list = instance.listProperty(fromPath: "List")!
|
||||
|
||||
XCTAssertEqual(list.count, 0)
|
||||
|
||||
let nestedInstance = file.viewModelNamed("Nested")!.createInstance()!
|
||||
list.append(nestedInstance)
|
||||
XCTAssertEqual(list.count, 1)
|
||||
XCTAssertIdentical(list.instance(at: 0), nestedInstance)
|
||||
|
||||
list.remove(nestedInstance)
|
||||
XCTAssertEqual(list.count, 0)
|
||||
XCTAssertNil(list.instance(at: 0))
|
||||
|
||||
let instance1 = file.viewModelNamed("Nested")!.createInstance()!
|
||||
let instance2 = file.viewModelNamed("Nested")!.createInstance()!
|
||||
list.append(instance1)
|
||||
list.append(instance2)
|
||||
XCTAssertEqual(list.count, 2)
|
||||
XCTAssertIdentical(list.instance(at: 0), instance1)
|
||||
XCTAssertIdentical(list.instance(at: 1), instance2)
|
||||
|
||||
list.remove(at: 0)
|
||||
XCTAssertEqual(list.count, 1)
|
||||
XCTAssertIdentical(list.instance(at: 0), instance2)
|
||||
|
||||
let instance3 = file.viewModelNamed("Nested")!.createInstance()!
|
||||
list.append(instance3)
|
||||
list.swap(at: 0, with: 1)
|
||||
XCTAssertIdentical(list.instance(at: 0), instance3)
|
||||
XCTAssertIdentical(list.instance(at: 1), instance2)
|
||||
|
||||
for i in (0..<list.count).reversed() {
|
||||
list.remove(at: Int32(i))
|
||||
}
|
||||
|
||||
let instance4 = file.viewModelNamed("Nested")!.createInstance()!
|
||||
let instance5 = file.viewModelNamed("Nested")!.createInstance()!
|
||||
let instance6 = file.viewModelNamed("Nested")!.createInstance()!
|
||||
XCTAssertFalse(list.insert(instance4, at: 100))
|
||||
XCTAssertTrue(list.insert(instance4, at: 0))
|
||||
XCTAssertTrue(list.insert(instance5, at: 1))
|
||||
// One final check to check false if index > list.count (currently 2)
|
||||
XCTAssertFalse(list.insert(instance6, at: 3))
|
||||
XCTAssertIdentical(list.instance(at: 0)!, instance4)
|
||||
XCTAssertEqual(list.count, 2)
|
||||
}
|
||||
|
||||
// MARK: Binding
|
||||
|
||||
Submodule submodules/rive-runtime updated: 07535cbf93...6f3badca69
Reference in New Issue
Block a user