From 1a4ebceb0cb7d9e5c7463316341db589c2e8c0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 8 Oct 2026 23:36:34 +0200 Subject: [PATCH 1/3] test: add a reproducer page for scripts missing the dataBind instance on iOS (#414) --- example/assets/rive/script_binding_probe.riv | Bin 0 -> 17434 bytes .../assets/rive/script_binding_probe.riv.d.ts | 28 +++++ .../src/reproducers/Issue414ScriptBinding.tsx | 108 ++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 example/assets/rive/script_binding_probe.riv create mode 100644 example/assets/rive/script_binding_probe.riv.d.ts create mode 100644 example/src/reproducers/Issue414ScriptBinding.tsx diff --git a/example/assets/rive/script_binding_probe.riv b/example/assets/rive/script_binding_probe.riv new file mode 100644 index 0000000000000000000000000000000000000000..baf103f2b2058b574ca33d0b9ab54c73898498c3 GIT binary patch literal 17434 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 +export default asset; diff --git a/example/src/reproducers/Issue414ScriptBinding.tsx b/example/src/reproducers/Issue414ScriptBinding.tsx new file mode 100644 index 00000000..d1abaa22 --- /dev/null +++ b/example/src/reproducers/Issue414ScriptBinding.tsx @@ -0,0 +1,108 @@ +import { useCallback, useState } from 'react'; +import { View, Text, StyleSheet, Pressable } from 'react-native'; +import { + Fit, + RiveView, + useRiveFile, + useRiveNumber, + useViewModelInstance, + type ViewModelInstance, +} from '@rive-app/react-native'; +import { type Metadata } from '../shared/metadata'; + +/** + * Issue #414: a script that caches context:viewModel() in init must see the + * instance passed through dataBind, not the file's default instance. + * + * The host seeds seed = 42 before mounting. The script sets isReady = 1 in + * init and copies seed into observedSeed every frame, drawing green when it + * sees 42 and red otherwise. Expected: 42 / 1 / 42 and a green square. + */ + +function Probe({ instance }: { instance: ViewModelInstance }) { + const { riveFile } = useRiveFile( + require('../../assets/rive/script_binding_probe.riv') + ); + const { value: seed } = useRiveNumber('seed', instance); + const { value: isReady } = useRiveNumber('isReady', instance); + const { value: observedSeed } = useRiveNumber('observedSeed', instance); + const pass = seed === 42 && isReady === 1 && observedSeed === 42; + + return ( + <> + + seed={String(seed)} isReady={String(isReady)} observedSeed= + {String(observedSeed)} + + + {pass ? 'PASS' : 'FAIL (script bound to another instance)'} + + {riveFile ? ( + + ) : null} + + ); +} + +export default function Issue414ScriptBinding() { + const { riveFile, error } = useRiveFile( + require('../../assets/rive/script_binding_probe.riv') + ); + const onInit = useCallback((vmi: ViewModelInstance) => { + vmi.numberProperty('seed')?.set(42); + }, []); + const { instance } = useViewModelInstance(riveFile, { + viewModelName: 'ProbeModel', + async: true, + onInit, + }); + const [generation, setGeneration] = useState(0); + + return ( + + {error ? {String(error)} : null} + {instance ? ( + + ) : ( + Loading… + )} + setGeneration((g) => g + 1)} + > + Remount + + + ); +} + +Issue414ScriptBinding.metadata = { + name: 'Issue #414: script view model binding', + description: 'Scripts cached the default instance instead of dataBind', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#fff', padding: 16 }, + values: { fontSize: 15, fontFamily: 'monospace' }, + verdict: { fontSize: 16, fontWeight: '700', marginVertical: 8 }, + pass: { color: '#2e7d32' }, + fail: { color: '#d32f2f' }, + rive: { width: 160, height: 160 }, + button: { + alignSelf: 'flex-start', + backgroundColor: '#007AFF', + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + marginTop: 16, + }, + buttonText: { color: '#fff', fontWeight: '600' }, +}); From 58d92481406ab5ebc4a77a127691ee631884270d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 8 Oct 2026 23:36:34 +0200 Subject: [PATCH 2/3] fix(ios): bind the view model instance when creating the state machine --- ios/new/RiveReactNativeView.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ios/new/RiveReactNativeView.swift b/ios/new/RiveReactNativeView.swift index 9c76e21d..407ec207 100644 --- a/ios/new/RiveReactNativeView.swift +++ b/ios/new/RiveReactNativeView.swift @@ -88,7 +88,6 @@ class RiveReactNativeView: UIView { guard let self else { return } do { let artboard = try await config.file.createArtboard(config.artboardName) - let stateMachine = try await artboard.createStateMachine(config.stateMachineName) let dataBind: RiveRuntime.DataBind switch config.bindData { @@ -113,6 +112,14 @@ class RiveReactNativeView: UIView { guard !Task.isCancelled else { return } + // Scripts read their view model when the state machine is created (#414). + let stateMachine: StateMachine + if case .instance(let vmi) = dataBind { + stateMachine = try await artboard.createStateMachine(config.stateMachineName, binding: vmi) + } else { + stateMachine = try await artboard.createStateMachine(config.stateMachineName) + } + let rive = try await RiveRuntime.Rive( file: config.file, artboard: artboard, From bcf05a1154455a7e52715a1f3de7ae81fec6095c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Fri, 9 Oct 2026 09:22:23 +0200 Subject: [PATCH 3/3] test: harness test for scripts seeing the dataBind instance (#414) Fails on main with isReady 0 / observedSeed 0; replaces the code comment. --- .../__tests__/script-databinding.harness.tsx | 84 +++++++++++++++++++ ios/new/RiveReactNativeView.swift | 1 - 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 example/__tests__/script-databinding.harness.tsx diff --git a/example/__tests__/script-databinding.harness.tsx b/example/__tests__/script-databinding.harness.tsx new file mode 100644 index 00000000..f7d596dd --- /dev/null +++ b/example/__tests__/script-databinding.harness.tsx @@ -0,0 +1,84 @@ +import { describe, it, expect, render, cleanup } from 'react-native-harness'; +import { View } from 'react-native'; +import { + RiveView, + RiveFileFactory, + Fit, + type ViewModelInstance, +} from '@rive-app/react-native'; + +// ProbeModel { seed, isReady, observedSeed }: the script caches +// context:viewModel() in init, sets isReady = 1 there, and copies seed into +// observedSeed every advance. +// Source: https://gist.github.com/luismolina13/693ab81ea35dd59136bdf8c94cb45c44 +const SCRIPT_BINDING_PROBE = require('../assets/rive/script_binding_probe.riv'); + +function expectDefined(value: T): asserts value is NonNullable { + expect(value).toBeDefined(); +} + +function number(instance: ViewModelInstance, name: string) { + const prop = instance.numberProperty(name); + expectDefined(prop); + return prop; +} + +async function waitForValues( + instance: ViewModelInstance, + expected: Record, + timeout = 5000 +) { + const names = Object.keys(expected); + const read = async () => + Object.fromEntries( + await Promise.all( + names.map(async (name) => [ + name, + await number(instance, name).getValueAsync(), + ]) + ) + ); + const deadline = Date.now() + timeout; + let actual = await read(); + while ( + names.some((name) => actual[name] !== expected[name]) && + Date.now() < deadline + ) { + await new Promise((resolve) => setTimeout(resolve, 50)); + actual = await read(); + } + return actual; +} + +describe('Scripts see the dataBind instance (issue #414)', () => { + it('script init and advance run against the instance passed to dataBind', async () => { + const file = await RiveFileFactory.fromSource( + SCRIPT_BINDING_PROBE, + undefined + ); + const vm = await file.viewModelByNameAsync('ProbeModel'); + expectDefined(vm); + const instance = await vm.createDefaultInstanceAsync(); + expectDefined(instance); + await number(instance, 'seed').setValueAsync(42); + + await render( + + + + ); + + const expected = { isReady: 1, observedSeed: 42 }; + expect(await waitForValues(instance, expected)).toEqual(expected); + + cleanup(); + }); +}); diff --git a/ios/new/RiveReactNativeView.swift b/ios/new/RiveReactNativeView.swift index 407ec207..82ddd62e 100644 --- a/ios/new/RiveReactNativeView.swift +++ b/ios/new/RiveReactNativeView.swift @@ -112,7 +112,6 @@ class RiveReactNativeView: UIView { guard !Task.isCancelled else { return } - // Scripts read their view model when the state machine is created (#414). let stateMachine: StateMachine if case .instance(let vmi) = dataBind { stateMachine = try await artboard.createStateMachine(config.stateMachineName, binding: vmi)