mirror of
https://github.com/rive-app/rive-ios.git
synced 2026-01-18 17:11:28 +01:00
feat(ios): add data binding replace view model instance support
## Changes - Adds support for replacing view model instance properties with other view model instances ## Fixes - Fixes an issue where nested view model instances and properties were getting incorrectly created and cached Diffs= 909ae012c1 feat(ios): add data binding replace view model instance support (#9500) Co-authored-by: David Skuza <david@rive.app>
This commit is contained in:
@@ -1 +1 @@
|
||||
598ad6c62e7b56024c97dad252774e5b935353c1
|
||||
909ae012c1e5c721d473e997fa61976a23acb768
|
||||
|
||||
@@ -131,6 +131,21 @@ NS_SWIFT_NAME(RiveDataBindingViewModel.Instance)
|
||||
- (nullable RiveDataBindingViewModelInstance*)viewModelInstancePropertyFromPath:
|
||||
(NSString*)path;
|
||||
|
||||
/// Replaces a view model property of the view model instance with another
|
||||
/// instance.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path to the view model property to replace.
|
||||
/// - instance: The instance to replace the view model property at `path`
|
||||
/// with.
|
||||
///
|
||||
/// - Returns: `true` if the view model instance was replaced, otherwise
|
||||
/// `false`.
|
||||
- (BOOL)setViewModelInstancePropertyFromPath:(NSString*)path
|
||||
toInstance:(RiveDataBindingViewModelInstance*)
|
||||
instance
|
||||
NS_SWIFT_NAME(setViewModelInstanceProperty(fromPath:to:));
|
||||
|
||||
/// Returns a trigger property in the view model instance.
|
||||
///
|
||||
/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type
|
||||
|
||||
@@ -264,7 +264,18 @@
|
||||
- (RiveDataBindingViewModelInstance*)viewModelInstancePropertyFromPath:
|
||||
(NSString*)path
|
||||
{
|
||||
return [self childForPath:path];
|
||||
RiveDataBindingViewModelInstance* parent = [self parentForPath:path];
|
||||
return
|
||||
[parent viewModelInstanceWithName:[[path pathComponents] lastObject]];
|
||||
}
|
||||
|
||||
- (BOOL)setViewModelInstancePropertyFromPath:(NSString*)path
|
||||
toInstance:(RiveDataBindingViewModelInstance*)
|
||||
instance
|
||||
{
|
||||
RiveDataBindingViewModelInstance* parent = [self parentForPath:path];
|
||||
return [parent setViewModelInstance:instance
|
||||
forName:[path lastPathComponent]];
|
||||
}
|
||||
|
||||
- (RiveDataBindingViewModelInstanceTriggerProperty*)triggerPropertyFromPath:
|
||||
@@ -341,29 +352,14 @@
|
||||
- (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];
|
||||
}
|
||||
}
|
||||
RiveDataBindingViewModelInstance* parent = [self parentForPath:path];
|
||||
[parent setProperty:value forName:[path lastPathComponent]];
|
||||
}
|
||||
|
||||
- (nullable id)cachedPropertyFromPath:(NSString*)path asClass:(Class)aClass
|
||||
{
|
||||
RiveDataBindingViewModelInstanceProperty* property =
|
||||
[_properties objectForKey:path];
|
||||
RiveDataBindingViewModelInstance* parent = [self parentForPath:path];
|
||||
id property = [parent cachedPropertyWithName:[path lastPathComponent]];
|
||||
if (property != nil && [property isKindOfClass:aClass])
|
||||
{
|
||||
return property;
|
||||
@@ -384,47 +380,68 @@
|
||||
|
||||
#pragma mark - Paths
|
||||
|
||||
- (nullable RiveDataBindingViewModelInstance*)childForPath:(NSString*)path
|
||||
- (nullable RiveDataBindingViewModelInstance*)parentForPath:(NSString*)path
|
||||
{
|
||||
NSArray* components = [path pathComponents];
|
||||
// If we have no components, we have no child to add
|
||||
if (components.count == 0)
|
||||
NSArray* pathComponents = [path pathComponents];
|
||||
|
||||
if (pathComponents.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]))
|
||||
if (pathComponents.count == 1)
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
NSArray* subpathComponents = [pathComponents
|
||||
subarrayWithRange:NSMakeRange(1, pathComponents.count - 1)];
|
||||
RiveDataBindingViewModelInstance* instance =
|
||||
[self viewModelInstanceWithName:[pathComponents firstObject]];
|
||||
return [instance
|
||||
parentForPath:[subpathComponents componentsJoinedByString:@"/"]];
|
||||
}
|
||||
|
||||
- (nullable RiveDataBindingViewModelInstance*)viewModelInstanceWithName:
|
||||
(NSString*)name
|
||||
{
|
||||
RiveDataBindingViewModelInstance* existing;
|
||||
if ((existing = _children[name]))
|
||||
{
|
||||
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)
|
||||
auto i = _instance->propertyViewModel(std::string([name UTF8String]));
|
||||
if (i == nullptr)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
RiveDataBindingViewModelInstance* instance =
|
||||
[[RiveDataBindingViewModelInstance alloc] initWithInstance:i];
|
||||
_children[name] = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
RiveDataBindingViewModelInstance* child =
|
||||
[[RiveDataBindingViewModelInstance alloc] initWithInstance:instance];
|
||||
_children[currentPath] = child;
|
||||
if (components.count == 1)
|
||||
- (BOOL)setViewModelInstance:(RiveDataBindingViewModelInstance*)instance
|
||||
forName:(NSString*)name
|
||||
{
|
||||
BOOL replaced = _instance->replaceViewModelByName(
|
||||
std::string([name UTF8String]), instance.instance);
|
||||
if (replaced)
|
||||
{
|
||||
return child;
|
||||
}
|
||||
else
|
||||
{
|
||||
NSArray* subpath =
|
||||
[components subarrayWithRange:NSMakeRange(1, components.count - 1)];
|
||||
return [self childForPath:[subpath componentsJoinedByString:@"/"]];
|
||||
_children[name] = instance;
|
||||
}
|
||||
return replaced;
|
||||
}
|
||||
|
||||
- (id)cachedPropertyWithName:(NSString*)name
|
||||
{
|
||||
return [_properties objectForKey:name];
|
||||
}
|
||||
|
||||
- (void)setProperty:(RiveDataBindingViewModelInstanceProperty*)property
|
||||
forName:(NSString*)name
|
||||
{
|
||||
_properties[name] = property;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Binary file not shown.
@@ -380,6 +380,58 @@ class DataBindingTests: XCTestCase {
|
||||
XCTAssertEqual(nestedProperty!.value, newValue)
|
||||
}
|
||||
|
||||
func test_viewModelInstance_nested_areIdentical() {
|
||||
let instance = file.viewModelNamed("Test")!.createDefaultInstance()!
|
||||
|
||||
let fromPath = instance.viewModelInstanceProperty(fromPath: "Nested/DeeperNested")!
|
||||
let nested = instance.viewModelInstanceProperty(fromPath: "Nested")!.viewModelInstanceProperty(fromPath: "DeeperNested")!
|
||||
XCTAssertIdentical(fromPath, nested)
|
||||
}
|
||||
|
||||
func test_viewModelInstance_replace_withCorrectInstance_setsNewInstance() throws {
|
||||
let instance = file.viewModelNamed("Test")!.createDefaultInstance()!
|
||||
XCTAssertEqual(instance.stringProperty(fromPath: "Nested/String")!.value, "Nested")
|
||||
|
||||
// Test one-level deep + caching
|
||||
let replacement = file.viewModelNamed("Nested")!.createInstance()!
|
||||
replacement.stringProperty(fromPath: "String")!.value = "Hello, Rive"
|
||||
var replaced = instance.setViewModelInstanceProperty(fromPath: "Nested", to: replacement)
|
||||
XCTAssertTrue(replaced)
|
||||
XCTAssertEqual(instance.stringProperty(fromPath: "Nested/String")!.value, "Hello, Rive")
|
||||
XCTAssertIdentical(
|
||||
instance.viewModelInstanceProperty(fromPath: "Nested"),
|
||||
replacement
|
||||
)
|
||||
|
||||
// Test two-level deep traversal + caching
|
||||
let nestedReplacement = file.viewModelNamed("Default")!.createDefaultInstance()!
|
||||
replaced = instance.setViewModelInstanceProperty(fromPath: "Nested/DeeperNested", to: nestedReplacement)
|
||||
XCTAssertTrue(replaced)
|
||||
XCTAssertIdentical(
|
||||
instance.viewModelInstanceProperty(fromPath: "Nested/DeeperNested")!,
|
||||
nestedReplacement
|
||||
)
|
||||
|
||||
XCTAssertIdentical(
|
||||
instance.viewModelInstanceProperty(fromPath: "Nested/DeeperNested")!,
|
||||
instance.viewModelInstanceProperty(fromPath: "Nested")!.viewModelInstanceProperty(fromPath: "DeeperNested")
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
instance.stringProperty(fromPath: "Nested/DeeperNested/String")!.value,
|
||||
instance.viewModelInstanceProperty(fromPath: "Nested")!
|
||||
.viewModelInstanceProperty(fromPath: "DeeperNested")!
|
||||
.stringProperty(fromPath: "String")!.value
|
||||
)
|
||||
}
|
||||
|
||||
func test_viewModelInstance_replace_withIncorrectInstance_returnsFalse() throws {
|
||||
let instance = file.viewModelNamed("Test")!.createDefaultInstance()!
|
||||
let replacement = file.viewModelNamed("Default")!.createInstance()!
|
||||
let replaced = instance.setViewModelInstanceProperty(fromPath: "Nested", to: replacement)
|
||||
XCTAssertFalse(replaced)
|
||||
}
|
||||
|
||||
// MARK: Trigger
|
||||
|
||||
func test_viewModelInstance_triggerProperty_returnsPropertyOrNil() {
|
||||
@@ -957,16 +1009,6 @@ class DataBindingTests: XCTestCase {
|
||||
wait(for: [expectation], timeout: 1)
|
||||
}
|
||||
|
||||
func test_nestedViewModel_property_createsAndCachesChildren() throws {
|
||||
let instance = file.viewModelNamed("Test")!.createDefaultInstance()!
|
||||
let nested: RiveDataBindingViewModel.Instance? = instance.viewModelInstanceProperty(fromPath: "Nested")!
|
||||
let nestedCopy = instance.viewModelInstanceProperty(fromPath: "Nested")!
|
||||
XCTAssertTrue(nested === nestedCopy)
|
||||
|
||||
let fullPath = instance.viewModelInstanceProperty(fromPath: "Nested/Nested")
|
||||
XCTAssertTrue(nestedCopy === fullPath)
|
||||
}
|
||||
|
||||
func test_instance_property_whenBoundToTwoStateMachines_ifNoAdditionalChanges_callsListenerOnce() throws {
|
||||
let instance = file.viewModelNamed("Test")!.createDefaultInstance()!
|
||||
|
||||
|
||||
Reference in New Issue
Block a user