diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelInstance.kt b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelInstance.kt
index 72b3d59..01c006e 100644
--- a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelInstance.kt
+++ b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelInstance.kt
@@ -56,4 +56,13 @@ class HybridViewModelInstance(val viewModelInstance: ViewModelInstance) : Hybrid
override fun artboardProperty(path: String) = getPropertyOrNull {
HybridViewModelArtboardProperty(viewModelInstance.getArtboardProperty(path))
}
+
+ override fun viewModel(path: String) = getPropertyOrNull {
+ HybridViewModelInstance(viewModelInstance.getInstanceProperty(path))
+ }
+
+ override fun replaceViewModel(path: String, instance: HybridViewModelInstanceSpec) {
+ val nativeInstance = (instance as HybridViewModelInstance).viewModelInstance
+ viewModelInstance.setInstanceProperty(path, nativeInstance)
+ }
}
diff --git a/example/assets/rive/viewmodelproperty.riv b/example/assets/rive/viewmodelproperty.riv
new file mode 100644
index 0000000..b487a80
Binary files /dev/null and b/example/assets/rive/viewmodelproperty.riv differ
diff --git a/example/src/pages/NestedViewModelExample.tsx b/example/src/pages/NestedViewModelExample.tsx
new file mode 100644
index 0000000..aae1264
--- /dev/null
+++ b/example/src/pages/NestedViewModelExample.tsx
@@ -0,0 +1,251 @@
+import {
+ View,
+ Text,
+ StyleSheet,
+ ActivityIndicator,
+ Button,
+ TextInput,
+} from 'react-native';
+import { useMemo, useRef, useState } from 'react';
+import {
+ Fit,
+ RiveView,
+ useRiveFile,
+ useRiveString,
+ type ViewModelInstance,
+ type RiveFile,
+ type RiveViewRef,
+} from '@rive-app/react-native';
+import { type Metadata } from '../helpers/metadata';
+
+export default function NestedViewModelExample() {
+ const { riveFile, isLoading, error } = useRiveFile(
+ require('../../assets/rive/viewmodelproperty.riv')
+ );
+
+ return (
+
+ {isLoading ? (
+
+ ) : riveFile ? (
+
+ ) : (
+ {error || 'Unexpected error'}
+ )}
+
+ );
+}
+
+function WithViewModelSetup({ file }: { file: RiveFile }) {
+ const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]);
+ const instance = useMemo(
+ () => viewModel?.createDefaultInstance(),
+ [viewModel]
+ );
+
+ if (!instance || !viewModel) {
+ return (
+
+ {!viewModel
+ ? 'No view model found'
+ : 'Failed to create view model instance'}
+
+ );
+ }
+
+ return ;
+}
+
+function ReplaceViewModelTest({
+ instance,
+ file,
+}: {
+ instance: ViewModelInstance;
+ file: RiveFile;
+}) {
+ const riveRef = useRef(null);
+ const [replaced, setReplaced] = useState(false);
+ const [log, setLog] = useState([]);
+
+ const { value: vm1Name, setValue: setVm1Name } = useRiveString(
+ 'vm1/name',
+ instance
+ );
+ const { value: vm2Name, setValue: setVm2Name } = useRiveString(
+ 'vm2/name',
+ instance
+ );
+
+ const handleSetVm1Name = (newValue: string) => {
+ setVm1Name(newValue);
+ riveRef.current?.playIfNeeded();
+ };
+
+ const handleSetVm2Name = (newValue: string) => {
+ setVm2Name(newValue);
+ riveRef.current?.playIfNeeded();
+ };
+
+ const addLog = (msg: string) => setLog((prev) => [...prev, msg]);
+
+ const handleReplace = () => {
+ // Get vm2's instance
+ const vm2Instance = instance.viewModel('vm2');
+ if (!vm2Instance) {
+ addLog('❌ viewModel("vm2") returned undefined');
+ return;
+ }
+ addLog(`✅ Got vm2 instance: ${vm2Instance.instanceName}`);
+
+ // Replace vm1 with vm2's instance
+ try {
+ instance.replaceViewModel('vm1', vm2Instance);
+ addLog('✅ replaceViewModel("vm1", vm2Instance) succeeded');
+ // Call playIfNeeded to update the graphics
+ riveRef.current?.playIfNeeded();
+ addLog('✅ Called playIfNeeded() to refresh display');
+ addLog('→ Now vm1 and vm2 point to the same instance');
+ addLog('→ Changing vm2.name should also change vm1.name');
+ setReplaced(true);
+ } catch (error) {
+ addLog(`❌ replaceViewModel failed: ${error}`);
+ }
+ };
+
+ return (
+
+ (riveRef.current = ref) }}
+ style={styles.rive}
+ autoPlay={true}
+ dataBind={instance}
+ fit={Fit.Contain}
+ file={file}
+ />
+
+
+ replaceViewModel() Test
+
+ Replace vm1 with vm2's instance. After replacement, changing vm2.name
+ should also change vm1.name since they share the same instance.
+
+
+
+ vm1.name:
+
+
+
+
+ vm2.name:
+
+
+
+ {!replaced && (
+
+ )}
+
+ {replaced && (
+
+ ✅ Replaced! Try changing vm2.name - vm1.name should update too
+
+ )}
+
+ {log.length > 0 && (
+
+ Log:
+ {log.map((entry, i) => (
+
+ {entry}
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+NestedViewModelExample.metadata = {
+ name: 'Nested ViewModel',
+ description:
+ 'Tests viewModel() and replaceViewModel() for nested ViewModel instances',
+} satisfies Metadata;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ },
+ content: {
+ flex: 1,
+ },
+ rive: {
+ flex: 1,
+ width: '100%',
+ },
+ info: {
+ padding: 16,
+ backgroundColor: '#fff',
+ gap: 12,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ },
+ description: {
+ fontSize: 14,
+ color: '#666',
+ },
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: '600',
+ width: 80,
+ },
+ input: {
+ flex: 1,
+ borderWidth: 1,
+ borderColor: '#ccc',
+ borderRadius: 8,
+ padding: 8,
+ fontSize: 14,
+ },
+ success: {
+ color: 'green',
+ fontWeight: '600',
+ },
+ logContainer: {
+ marginTop: 8,
+ padding: 8,
+ backgroundColor: '#f5f5f5',
+ borderRadius: 8,
+ },
+ logTitle: {
+ fontWeight: '600',
+ marginBottom: 4,
+ },
+ logEntry: {
+ fontSize: 12,
+ fontFamily: 'monospace',
+ color: '#333',
+ },
+ errorText: {
+ color: 'red',
+ textAlign: 'center',
+ padding: 20,
+ },
+});
diff --git a/example/src/pages/index.ts b/example/src/pages/index.ts
index 12b2b84..f151239 100644
--- a/example/src/pages/index.ts
+++ b/example/src/pages/index.ts
@@ -12,3 +12,4 @@ export { default as ResponsiveLayouts } from './ResponsiveLayouts';
export { default as SharedValueListenerExample } from './SharedValueListenerExample';
export { default as MenuListExample } from './MenuListExample';
export { default as DataBindingArtboardsExample } from './DataBindingArtboardsExample';
+export { default as NestedViewModelExample } from './NestedViewModelExample';
diff --git a/ios/HybridViewModelInstance.swift b/ios/HybridViewModelInstance.swift
index 7989afd..7cd3d89 100644
--- a/ios/HybridViewModelInstance.swift
+++ b/ios/HybridViewModelInstance.swift
@@ -53,4 +53,20 @@ class HybridViewModelInstance: HybridViewModelInstanceSpec {
guard let property = viewModelInstance?.artboardProperty(fromPath: path) else { return nil }
return HybridViewModelArtboardProperty(property: property)
}
+
+ func viewModel(path: String) throws -> (any HybridViewModelInstanceSpec)? {
+ guard let instance = viewModelInstance?.viewModelInstanceProperty(fromPath: path) else { return nil }
+ return HybridViewModelInstance(viewModelInstance: instance)
+ }
+
+ func replaceViewModel(path: String, instance: any HybridViewModelInstanceSpec) throws {
+ guard let hybridInstance = instance as? HybridViewModelInstance,
+ let nativeInstance = hybridInstance.viewModelInstance else {
+ throw RuntimeError.error(withMessage: "Invalid ViewModelInstance provided to replaceViewModel")
+ }
+ let success = viewModelInstance?.setViewModelInstanceProperty(fromPath: path, to: nativeInstance) ?? false
+ if !success {
+ throw RuntimeError.error(withMessage: "Failed to replace ViewModel at path: \(path)")
+ }
+ }
}
diff --git a/nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.cpp b/nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.cpp
index f28f39a..da3984b 100644
--- a/nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.cpp
+++ b/nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.cpp
@@ -25,6 +25,8 @@ namespace margelo::nitro::rive { class HybridViewModelImagePropertySpec; }
namespace margelo::nitro::rive { class HybridViewModelListPropertySpec; }
// Forward declaration of `HybridViewModelArtboardPropertySpec` to properly resolve imports.
namespace margelo::nitro::rive { class HybridViewModelArtboardPropertySpec; }
+// Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports.
+namespace margelo::nitro::rive { class HybridViewModelInstanceSpec; }
#include
#include
@@ -47,6 +49,8 @@ namespace margelo::nitro::rive { class HybridViewModelArtboardPropertySpec; }
#include "JHybridViewModelListPropertySpec.hpp"
#include "HybridViewModelArtboardPropertySpec.hpp"
#include "JHybridViewModelArtboardPropertySpec.hpp"
+#include "HybridViewModelInstanceSpec.hpp"
+#include "JHybridViewModelInstanceSpec.hpp"
namespace margelo::nitro::rive {
@@ -129,5 +133,14 @@ namespace margelo::nitro::rive {
auto __result = method(_javaPart, jni::make_jstring(path));
return __result != nullptr ? std::make_optional(__result->cthis()->shared_cast()) : std::nullopt;
}
+ std::optional> JHybridViewModelInstanceSpec::viewModel(const std::string& path) {
+ static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* path */)>("viewModel");
+ auto __result = method(_javaPart, jni::make_jstring(path));
+ return __result != nullptr ? std::make_optional(__result->cthis()->shared_cast()) : std::nullopt;
+ }
+ void JHybridViewModelInstanceSpec::replaceViewModel(const std::string& path, const std::shared_ptr& instance) {
+ static const auto method = javaClassStatic()->getMethod /* path */, jni::alias_ref /* instance */)>("replaceViewModel");
+ method(_javaPart, jni::make_jstring(path), std::dynamic_pointer_cast(instance)->getJavaPart());
+ }
} // namespace margelo::nitro::rive
diff --git a/nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.hpp b/nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.hpp
index c525dcc..cc57458 100644
--- a/nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.hpp
+++ b/nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.hpp
@@ -63,6 +63,8 @@ namespace margelo::nitro::rive {
std::optional> imageProperty(const std::string& path) override;
std::optional> listProperty(const std::string& path) override;
std::optional> artboardProperty(const std::string& path) override;
+ std::optional> viewModel(const std::string& path) override;
+ void replaceViewModel(const std::string& path, const std::shared_ptr& instance) override;
private:
friend HybridBase;
diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridViewModelInstanceSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridViewModelInstanceSpec.kt
index 8bdc2d1..24e478d 100644
--- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridViewModelInstanceSpec.kt
+++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridViewModelInstanceSpec.kt
@@ -82,6 +82,14 @@ abstract class HybridViewModelInstanceSpec: HybridObject() {
@DoNotStrip
@Keep
abstract fun artboardProperty(path: String): HybridViewModelArtboardPropertySpec?
+
+ @DoNotStrip
+ @Keep
+ abstract fun viewModel(path: String): HybridViewModelInstanceSpec?
+
+ @DoNotStrip
+ @Keep
+ abstract fun replaceViewModel(path: String, instance: HybridViewModelInstanceSpec): Unit
private external fun initHybrid(): HybridData
diff --git a/nitrogen/generated/ios/c++/HybridViewModelInstanceSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridViewModelInstanceSpecSwift.hpp
index 67008bf..48e316f 100644
--- a/nitrogen/generated/ios/c++/HybridViewModelInstanceSpecSwift.hpp
+++ b/nitrogen/generated/ios/c++/HybridViewModelInstanceSpecSwift.hpp
@@ -30,6 +30,8 @@ namespace margelo::nitro::rive { class HybridViewModelImagePropertySpec; }
namespace margelo::nitro::rive { class HybridViewModelListPropertySpec; }
// Forward declaration of `HybridViewModelArtboardPropertySpec` to properly resolve imports.
namespace margelo::nitro::rive { class HybridViewModelArtboardPropertySpec; }
+// Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports.
+namespace margelo::nitro::rive { class HybridViewModelInstanceSpec; }
#include
#include
@@ -43,6 +45,7 @@ namespace margelo::nitro::rive { class HybridViewModelArtboardPropertySpec; }
#include "HybridViewModelImagePropertySpec.hpp"
#include "HybridViewModelListPropertySpec.hpp"
#include "HybridViewModelArtboardPropertySpec.hpp"
+#include "HybridViewModelInstanceSpec.hpp"
#include "RNRive-Swift-Cxx-Umbrella.hpp"
@@ -163,6 +166,20 @@ namespace margelo::nitro::rive {
auto __value = std::move(__result.value());
return __value;
}
+ inline std::optional> viewModel(const std::string& path) override {
+ auto __result = _swiftPart.viewModel(path);
+ if (__result.hasError()) [[unlikely]] {
+ std::rethrow_exception(__result.error());
+ }
+ auto __value = std::move(__result.value());
+ return __value;
+ }
+ inline void replaceViewModel(const std::string& path, const std::shared_ptr& instance) override {
+ auto __result = _swiftPart.replaceViewModel(path, instance);
+ if (__result.hasError()) [[unlikely]] {
+ std::rethrow_exception(__result.error());
+ }
+ }
private:
RNRive::HybridViewModelInstanceSpec_cxx _swiftPart;
diff --git a/nitrogen/generated/ios/swift/HybridViewModelInstanceSpec.swift b/nitrogen/generated/ios/swift/HybridViewModelInstanceSpec.swift
index a9a41a5..436f52c 100644
--- a/nitrogen/generated/ios/swift/HybridViewModelInstanceSpec.swift
+++ b/nitrogen/generated/ios/swift/HybridViewModelInstanceSpec.swift
@@ -23,6 +23,8 @@ public protocol HybridViewModelInstanceSpec_protocol: HybridObject {
func imageProperty(path: String) throws -> (any HybridViewModelImagePropertySpec)?
func listProperty(path: String) throws -> (any HybridViewModelListPropertySpec)?
func artboardProperty(path: String) throws -> (any HybridViewModelArtboardPropertySpec)?
+ func viewModel(path: String) throws -> (any HybridViewModelInstanceSpec)?
+ func replaceViewModel(path: String, instance: (any HybridViewModelInstanceSpec)) throws -> Void
}
public extension HybridViewModelInstanceSpec_protocol {
diff --git a/nitrogen/generated/ios/swift/HybridViewModelInstanceSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridViewModelInstanceSpec_cxx.swift
index 3001b16..9bda15b 100644
--- a/nitrogen/generated/ios/swift/HybridViewModelInstanceSpec_cxx.swift
+++ b/nitrogen/generated/ios/swift/HybridViewModelInstanceSpec_cxx.swift
@@ -310,4 +310,40 @@ open class HybridViewModelInstanceSpec_cxx {
return bridge.create_Result_std__optional_std__shared_ptr_HybridViewModelArtboardPropertySpec___(__exceptionPtr)
}
}
+
+ @inline(__always)
+ public final func viewModel(path: std.string) -> bridge.Result_std__optional_std__shared_ptr_HybridViewModelInstanceSpec___ {
+ do {
+ let __result = try self.__implementation.viewModel(path: String(path))
+ let __resultCpp = { () -> bridge.std__optional_std__shared_ptr_HybridViewModelInstanceSpec__ in
+ if let __unwrappedValue = __result {
+ return bridge.create_std__optional_std__shared_ptr_HybridViewModelInstanceSpec__({ () -> bridge.std__shared_ptr_HybridViewModelInstanceSpec_ in
+ let __cxxWrapped = __unwrappedValue.getCxxWrapper()
+ return __cxxWrapped.getCxxPart()
+ }())
+ } else {
+ return .init()
+ }
+ }()
+ return bridge.create_Result_std__optional_std__shared_ptr_HybridViewModelInstanceSpec___(__resultCpp)
+ } catch (let __error) {
+ let __exceptionPtr = __error.toCpp()
+ return bridge.create_Result_std__optional_std__shared_ptr_HybridViewModelInstanceSpec___(__exceptionPtr)
+ }
+ }
+
+ @inline(__always)
+ public final func replaceViewModel(path: std.string, instance: bridge.std__shared_ptr_HybridViewModelInstanceSpec_) -> bridge.Result_void_ {
+ do {
+ try self.__implementation.replaceViewModel(path: String(path), instance: { () -> HybridViewModelInstanceSpec in
+ let __unsafePointer = bridge.get_std__shared_ptr_HybridViewModelInstanceSpec_(instance)
+ let __instance = HybridViewModelInstanceSpec_cxx.fromUnsafe(__unsafePointer)
+ return __instance.getHybridViewModelInstanceSpec()
+ }())
+ return bridge.create_Result_void_()
+ } catch (let __error) {
+ let __exceptionPtr = __error.toCpp()
+ return bridge.create_Result_void_(__exceptionPtr)
+ }
+ }
}
diff --git a/nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.cpp b/nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.cpp
index 3e33e47..5f69efa 100644
--- a/nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.cpp
+++ b/nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.cpp
@@ -24,6 +24,8 @@ namespace margelo::nitro::rive {
prototype.registerHybridMethod("imageProperty", &HybridViewModelInstanceSpec::imageProperty);
prototype.registerHybridMethod("listProperty", &HybridViewModelInstanceSpec::listProperty);
prototype.registerHybridMethod("artboardProperty", &HybridViewModelInstanceSpec::artboardProperty);
+ prototype.registerHybridMethod("viewModel", &HybridViewModelInstanceSpec::viewModel);
+ prototype.registerHybridMethod("replaceViewModel", &HybridViewModelInstanceSpec::replaceViewModel);
});
}
diff --git a/nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.hpp b/nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.hpp
index ea4b374..e14e95d 100644
--- a/nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.hpp
+++ b/nitrogen/generated/shared/c++/HybridViewModelInstanceSpec.hpp
@@ -31,6 +31,8 @@ namespace margelo::nitro::rive { class HybridViewModelImagePropertySpec; }
namespace margelo::nitro::rive { class HybridViewModelListPropertySpec; }
// Forward declaration of `HybridViewModelArtboardPropertySpec` to properly resolve imports.
namespace margelo::nitro::rive { class HybridViewModelArtboardPropertySpec; }
+// Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports.
+namespace margelo::nitro::rive { class HybridViewModelInstanceSpec; }
#include
#include
@@ -44,6 +46,7 @@ namespace margelo::nitro::rive { class HybridViewModelArtboardPropertySpec; }
#include "HybridViewModelImagePropertySpec.hpp"
#include "HybridViewModelListPropertySpec.hpp"
#include "HybridViewModelArtboardPropertySpec.hpp"
+#include "HybridViewModelInstanceSpec.hpp"
namespace margelo::nitro::rive {
@@ -85,6 +88,8 @@ namespace margelo::nitro::rive {
virtual std::optional> imageProperty(const std::string& path) = 0;
virtual std::optional> listProperty(const std::string& path) = 0;
virtual std::optional> artboardProperty(const std::string& path) = 0;
+ virtual std::optional> viewModel(const std::string& path) = 0;
+ virtual void replaceViewModel(const std::string& path, const std::shared_ptr& instance) = 0;
protected:
// Hybrid Setup
diff --git a/src/specs/ViewModel.nitro.ts b/src/specs/ViewModel.nitro.ts
index b9b607c..e734125 100644
--- a/src/specs/ViewModel.nitro.ts
+++ b/src/specs/ViewModel.nitro.ts
@@ -59,6 +59,19 @@ export interface ViewModelInstance
/** Get an artboard property from the view model instance at the given path */
artboardProperty(path: string): ViewModelArtboardProperty | undefined;
+
+ /**
+ * Get a nested ViewModel instance at the given path.
+ * Supports path notation with "/" for nested access (e.g., "Parent/Child").
+ */
+ viewModel(path: string): ViewModelInstance | undefined;
+
+ /**
+ * Replace the ViewModel instance at the given path with a new instance.
+ * The replacement instance must be compatible with the expected ViewModel type.
+ * @throws Error if path not found or types incompatible
+ */
+ replaceViewModel(path: string, instance: ViewModelInstance): void;
}
export interface ViewModelProperty