Skip to content

Commit 65196b5

Browse files
committed
feat: add viewModelInstanceProperty for nested ViewModel access
Adds two methods to ViewModelInstance: - viewModelInstanceProperty(path) - get nested ViewModel instance - setViewModelInstanceProperty(path, instance) - replace nested instance Includes test example with person_databinding_test.riv
1 parent 6052279 commit 65196b5

14 files changed

Lines changed: 328 additions & 0 deletions

android/src/main/java/com/margelo/nitro/rive/HybridViewModelInstance.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,18 @@ class HybridViewModelInstance(val viewModelInstance: ViewModelInstance) : Hybrid
5252
override fun listProperty(path: String) = getPropertyOrNull {
5353
HybridViewModelListProperty(viewModelInstance.getListProperty(path))
5454
}
55+
56+
override fun viewModelInstanceProperty(path: String) = getPropertyOrNull {
57+
HybridViewModelInstance(viewModelInstance.getInstanceProperty(path))
58+
}
59+
60+
override fun setViewModelInstanceProperty(path: String, instance: HybridViewModelInstanceSpec): Boolean {
61+
return try {
62+
val nativeInstance = (instance as HybridViewModelInstance).viewModelInstance
63+
viewModelInstance.setInstanceProperty(path, nativeInstance)
64+
true
65+
} catch (e: ViewModelException) {
66+
false
67+
}
68+
}
5569
}
792 KB
Binary file not shown.
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import {
2+
View,
3+
Text,
4+
StyleSheet,
5+
ActivityIndicator,
6+
Button,
7+
} from 'react-native';
8+
import { useEffect, useMemo, useState } from 'react';
9+
import {
10+
Fit,
11+
RiveView,
12+
useRiveFile,
13+
useRiveEnum,
14+
type ViewModelInstance,
15+
type RiveFile,
16+
} from '@rive-app/react-native';
17+
import { type Metadata } from '../helpers/metadata';
18+
19+
export default function NestedViewModelExample() {
20+
const { riveFile, isLoading, error } = useRiveFile(
21+
require('../../assets/rive/person_databinding_test.riv')
22+
);
23+
24+
return (
25+
<View style={styles.container}>
26+
<View style={styles.riveContainer}>
27+
{isLoading ? (
28+
<ActivityIndicator size="large" color="#0000ff" />
29+
) : riveFile ? (
30+
<WithViewModelSetup file={riveFile} />
31+
) : (
32+
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
33+
)}
34+
</View>
35+
</View>
36+
);
37+
}
38+
39+
function WithViewModelSetup({ file }: { file: RiveFile }) {
40+
const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]);
41+
const instance = useMemo(
42+
() => viewModel?.createDefaultInstance(),
43+
[viewModel]
44+
);
45+
46+
if (!instance || !viewModel) {
47+
return (
48+
<Text style={styles.errorText}>
49+
{!viewModel
50+
? 'No view model found'
51+
: 'Failed to create view model instance'}
52+
</Text>
53+
);
54+
}
55+
56+
return <NestedViewModelTest instance={instance} file={file} />;
57+
}
58+
59+
function NestedViewModelTest({
60+
instance,
61+
file,
62+
}: {
63+
instance: ViewModelInstance;
64+
file: RiveFile;
65+
}) {
66+
const [testResults, setTestResults] = useState<string[]>([]);
67+
68+
// Test 1: Path notation (like rive-react does)
69+
const { value: drinkType, setValue: setDrinkType } = useRiveEnum(
70+
'favDrink/type',
71+
instance
72+
);
73+
74+
useEffect(() => {
75+
const results: string[] = [];
76+
77+
// Test 1: viewModelInstanceProperty - get nested instance
78+
const nestedDrink = instance.viewModelInstanceProperty('favDrink');
79+
if (nestedDrink) {
80+
results.push(`✅ viewModelInstanceProperty('favDrink') returned instance`);
81+
results.push(` Instance name: ${nestedDrink.instanceName}`);
82+
83+
// Try to get enum property from nested instance
84+
const typeFromNested = nestedDrink.enumProperty('type');
85+
if (typeFromNested) {
86+
results.push(`✅ nestedDrink.enumProperty('type'): ${typeFromNested.value}`);
87+
} else {
88+
results.push(`❌ nestedDrink.enumProperty('type') returned undefined`);
89+
}
90+
} else {
91+
results.push(`❌ viewModelInstanceProperty('favDrink') returned undefined`);
92+
results.push(` (This file may use path notation, not ViewModel instance properties)`);
93+
}
94+
95+
// Test 2: Direct path notation
96+
const directEnum = instance.enumProperty('favDrink/type');
97+
if (directEnum) {
98+
results.push(`✅ enumProperty('favDrink/type'): ${directEnum.value}`);
99+
} else {
100+
results.push(`❌ enumProperty('favDrink/type') returned undefined`);
101+
}
102+
103+
setTestResults(results);
104+
}, [instance]);
105+
106+
// Test 3: useRiveEnum hook result (shown separately as it updates reactively)
107+
const useRiveEnumResult = drinkType
108+
? `✅ useRiveEnum('favDrink/type'): ${drinkType}`
109+
: `❌ useRiveEnum('favDrink/type'): undefined`;
110+
111+
return (
112+
<View style={styles.content}>
113+
<RiveView
114+
style={styles.rive}
115+
autoPlay={true}
116+
dataBind={instance}
117+
fit={Fit.Contain}
118+
file={file}
119+
/>
120+
121+
<View style={styles.info}>
122+
<Text style={styles.title}>Test Results:</Text>
123+
{testResults.map((result, i) => (
124+
<Text key={i} style={styles.result}>
125+
{result}
126+
</Text>
127+
))}
128+
<Text style={styles.result}>{useRiveEnumResult}</Text>
129+
130+
<View style={styles.buttons}>
131+
<Button title="Coffee" onPress={() => setDrinkType('Coffee')} />
132+
<Button title="Tea" onPress={() => setDrinkType('Tea')} />
133+
</View>
134+
</View>
135+
</View>
136+
);
137+
}
138+
139+
NestedViewModelExample.metadata = {
140+
name: 'Nested ViewModel',
141+
description: 'Tests viewModelInstanceProperty() for accessing nested ViewModel instances',
142+
} satisfies Metadata;
143+
144+
const styles = StyleSheet.create({
145+
container: {
146+
flex: 1,
147+
backgroundColor: '#fff',
148+
},
149+
riveContainer: {
150+
flex: 1,
151+
backgroundColor: '#f5f5f5',
152+
},
153+
content: {
154+
flex: 1,
155+
},
156+
rive: {
157+
flex: 1,
158+
width: '100%',
159+
},
160+
info: {
161+
padding: 16,
162+
backgroundColor: '#fff',
163+
},
164+
title: {
165+
fontSize: 16,
166+
fontWeight: 'bold',
167+
marginBottom: 8,
168+
},
169+
result: {
170+
fontSize: 12,
171+
fontFamily: 'monospace',
172+
color: '#333',
173+
marginVertical: 2,
174+
},
175+
label: {
176+
fontSize: 14,
177+
color: '#666',
178+
marginTop: 12,
179+
},
180+
value: {
181+
fontSize: 18,
182+
fontWeight: 'bold',
183+
color: '#333',
184+
},
185+
buttons: {
186+
flexDirection: 'row',
187+
gap: 12,
188+
marginTop: 16,
189+
},
190+
errorText: {
191+
color: 'red',
192+
textAlign: 'center',
193+
padding: 20,
194+
},
195+
});

example/src/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export { default as ManyViewModels } from './ManyViewModels';
1111
export { default as ResponsiveLayouts } from './ResponsiveLayouts';
1212
export { default as SharedValueListenerExample } from './SharedValueListenerExample';
1313
export { default as MenuListExample } from './MenuListExample';
14+
export { default as NestedViewModelExample } from './NestedViewModelExample';

ios/HybridViewModelInstance.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,17 @@ class HybridViewModelInstance: HybridViewModelInstanceSpec {
4848
guard let property = viewModelInstance?.listProperty(fromPath: path) else { return nil }
4949
return HybridViewModelListProperty(property: property)
5050
}
51+
52+
func viewModelInstanceProperty(path: String) throws -> (any HybridViewModelInstanceSpec)? {
53+
guard let instance = viewModelInstance?.viewModelInstanceProperty(fromPath: path) else { return nil }
54+
return HybridViewModelInstance(viewModelInstance: instance)
55+
}
56+
57+
func setViewModelInstanceProperty(path: String, instance: any HybridViewModelInstanceSpec) throws -> Bool {
58+
guard let hybridInstance = instance as? HybridViewModelInstance,
59+
let nativeInstance = hybridInstance.viewModelInstance else {
60+
return false
61+
}
62+
return viewModelInstance?.setViewModelInstanceProperty(fromPath: path, to: nativeInstance) ?? false
63+
}
5164
}

nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.cpp

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.hpp

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridViewModelInstanceSpec.kt

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nitrogen/generated/ios/c++/HybridViewModelInstanceSpecSwift.hpp

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nitrogen/generated/ios/swift/HybridViewModelInstanceSpec.swift

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)