Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions example/__tests__/script-databinding.harness.tsx
Original file line number Diff line number Diff line change
@@ -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<T>(value: T): asserts value is NonNullable<T> {
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<string, number>,
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(
<View style={{ width: 160, height: 160 }}>
<RiveView
style={{ flex: 1 }}
file={file}
artboardName="Binding Probe"
stateMachineName="Main"
dataBind={instance}
autoPlay={true}
fit={Fit.Layout}
/>
</View>
);

const expected = { isReady: 1, observedSeed: 42 };
expect(await waitForValues(instance, expected)).toEqual(expected);

cleanup();
});
});
Binary file added example/assets/rive/script_binding_probe.riv
Binary file not shown.
28 changes: 28 additions & 0 deletions example/assets/rive/script_binding_probe.riv.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Generated by rive-gen-types — do not edit manually. @generated
/* eslint-disable */
// Source: script_binding_probe.riv
import type { RiveFileSource } from '@rive-app/react-native';
declare const asset: RiveFileSource<{
schemaVersion: 1;
artboards: 'Binding Probe' | 'NuRiveWordmark01' | 'NuRiveBrandmark01' | 'Watermark';
defaultArtboard: 'Binding Probe';
stateMachines: {
'Binding Probe': 'Main';
'NuRiveWordmark01': 'State Machine 1';
'NuRiveBrandmark01': 'State Machine 1';
'Watermark': 'State Machine 1';
};
enums: {};
viewModels: {
ProbeModel: {
seed: 'number';
observedSeed: 'number';
isReady: 'number';
};
RiveCLIBootUp: {

};
};
referencedAssets: {};
}>;
export default asset;
108 changes: 108 additions & 0 deletions example/src/reproducers/Issue414ScriptBinding.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Text style={styles.values} testID="issue414-values">
seed={String(seed)} isReady={String(isReady)} observedSeed=
{String(observedSeed)}
</Text>
<Text style={[styles.verdict, pass ? styles.pass : styles.fail]}>
{pass ? 'PASS' : 'FAIL (script bound to another instance)'}
</Text>
{riveFile ? (
<RiveView
file={riveFile}
artboardName="Binding Probe"
stateMachineName="Main"
dataBind={instance}
autoPlay
fit={Fit.Layout}
style={styles.rive}
/>
) : 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 (
<View style={styles.container}>
{error ? <Text>{String(error)}</Text> : null}
{instance ? (
<Probe key={generation} instance={instance} />
) : (
<Text>Loading…</Text>
)}
<Pressable
style={styles.button}
onPress={() => setGeneration((g) => g + 1)}
>
<Text style={styles.buttonText}>Remount</Text>
</Pressable>
</View>
);
}

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' },
});
8 changes: 7 additions & 1 deletion ios/new/RiveReactNativeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@
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 {
Expand All @@ -98,7 +97,7 @@
// Probe for a default ViewModel first. If the artboard has none,
// the SDK would fire an error event — skip auto-binding silently instead.
do {
let _ = try await config.file.getDefaultViewModelInfo(for: artboard)

Check warning on line 100 in ios/new/RiveReactNativeView.swift

View workflow job for this annotation

GitHub Actions / lint-swift

Prefer `_ = foo()` over `let _ = foo()` when discarding a result from a function (redundant_discardable_let)
dataBind = .auto
} catch {
dataBind = .none
Expand All @@ -113,6 +112,13 @@

guard !Task.isCancelled else { return }

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,
Expand Down
Loading