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
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-picker/picker": "^2.11.4",
"@react-navigation/native": "^7.1.9",
"@react-navigation/native-stack": "^7.3.16",
"@react-navigation/stack": "^7.3.2",
"react": "19.1.0",
"react-native": "0.80.3",
Expand Down
171 changes: 171 additions & 0 deletions example/src/reproducers/Issue356NativeStackBack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
Pressable,
Animated,
Easing,
} from 'react-native';
import {
createNativeStackNavigator,
type NativeStackNavigationProp,
} from '@react-navigation/native-stack';
import { RiveView, useRiveFile, Fit } from '@rive-app/react-native';
import { type Metadata } from '../shared/metadata';

/**
* Reproducer for issue #356: the Rive content disappeared part-way through an
* iOS native-stack close transition, while the rest of the outgoing screen kept
* sliding out.
*
* Open the Rive screen, then go back while the animation is moving. The blue
* control box is the reference — whatever happens to the Rive tiles has to
* happen to it too. Several tiles because the failure was intermittent
* (~7% of pops), so a grid catches it sooner.
*/

type ParamList = {
Issue356Start: undefined;
Issue356Animation: undefined;
};

const Stack = createNativeStackNavigator<ParamList>();
const TILES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];

function StartScreen({
navigation,
}: {
navigation: NativeStackNavigationProp<ParamList, 'Issue356Start'>;
}) {
return (
<View style={styles.container}>
<Text style={styles.title}>Issue #356</Text>
<Text style={styles.subtitle}>
Open the next screen, then go back while the animation is moving and
watch the outgoing screen slide away.
</Text>
<Pressable
style={styles.button}
onPress={() => navigation.navigate('Issue356Animation')}
>
<Text style={styles.buttonText}>Open Rive screen</Text>
</Pressable>
</View>
);
}

function AnimationScreen({
navigation,
}: {
navigation: NativeStackNavigationProp<ParamList, 'Issue356Animation'>;
}) {
const { riveFile } = useRiveFile(require('../../assets/rive/rewards.riv'));
const markerX = useRef(new Animated.Value(0)).current;

useEffect(() => {
const loop = Animated.loop(
Animated.sequence([
Animated.timing(markerX, {
toValue: 260,
duration: 900,
easing: Easing.linear,
useNativeDriver: true,
}),
Animated.timing(markerX, {
toValue: 0,
duration: 900,
easing: Easing.linear,
useNativeDriver: true,
}),
])
);
loop.start();
return () => loop.stop();
}, [markerX]);

return (
<View style={styles.container}>
<View style={styles.grid}>
{TILES.map((i) => (
<View key={i} style={styles.frame}>
{riveFile ? (
<RiveView file={riveFile} fit={Fit.Contain} style={styles.rive} />
) : null}
</View>
))}
</View>
<View style={styles.control}>
<Animated.View
style={[styles.marker, { transform: [{ translateX: markerX }] }]}
/>
<Text style={styles.controlText}>control</Text>
</View>
<Pressable style={styles.button} onPress={() => navigation.goBack()}>
<Text style={styles.buttonText}>Go back</Text>
</Pressable>
</View>
);
}

export default function Issue356NativeStackBack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Issue356Start" component={StartScreen} />
<Stack.Screen name="Issue356Animation" component={AnimationScreen} />
</Stack.Navigator>
);
}

Issue356NativeStackBack.metadata = {
name: 'Issue #356 native-stack back',
description:
'Rive content must stay visible until the iOS native-stack close transition finishes',
} satisfies Metadata;

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 20,
justifyContent: 'center',
},
title: { fontSize: 24, fontWeight: 'bold', marginBottom: 10 },
subtitle: { fontSize: 15, color: '#666', marginBottom: 20 },
grid: { flexDirection: 'row', flexWrap: 'wrap' },
frame: {
width: '33%',
height: 110,
backgroundColor: '#ffd9d9',
borderWidth: 2,
borderColor: '#d33',
},
rive: { flex: 1 },
control: {
marginTop: 12,
height: 80,
backgroundColor: '#d9e8ff',
borderWidth: 2,
borderColor: '#36c',
alignItems: 'center',
justifyContent: 'center',
},
controlText: { color: '#36c', fontWeight: 'bold' },
marker: {
position: 'absolute',
left: 8,
top: 8,
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: '#0a0',
},
button: {
marginTop: 20,
backgroundColor: '#323232',
paddingVertical: 14,
borderRadius: 8,
alignItems: 'center',
},
buttonText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
});
10 changes: 4 additions & 6 deletions ios/new/HybridRiveView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,10 @@ class HybridRiveView: HybridRiveViewSpec {

// MARK: Lifecycle
func dispose() {
// Nitro finalizes HybridObjects on the JS/GC thread; the view's teardown
// must run on main (mirrors the legacy backend's dispose()).
let riveView = view as? RiveReactNativeView
DispatchQueue.main.async {
riveView?.detach()
}
// Deliberately empty: the view tears itself down when Fabric drops it (see
// RiveReactNativeView.willMove(toSuperview:)). Doing it here instead would
// run in React's commit phase, ahead of the mounting transaction, which is
// what caused #356.
}

// MARK: Views
Expand Down
20 changes: 20 additions & 0 deletions ios/new/RiveReactNativeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,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 107 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)

Check warning on line 107 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 Down Expand Up @@ -285,6 +285,26 @@
pendingBindInstance = nil
}

/// Teardown runs when Fabric drops the view, not when the JS effect cleanup
/// calls dispose().
///
/// The effect cleanup fires in React's commit phase, before the mounting
/// instructions reach native views — potentially a whole transaction early.
/// react-native-screens captures the outgoing screen during that transaction
/// (`unmountChildComponentView`), and a render-server composite landing in the
/// gap bakes our half-torn-down view into that capture, so the screen slides
/// away empty (#356). Fabric's own unmount happens inside the same transaction
/// as the capture, leaving no room for one — which is why plain RN views never
/// show this.
override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
// Unconditional: a view whose configure failed has no riveUIView but can
// still have awaitViewReady() waiters, and detach() is what settles them.
if newSuperview == nil {
detach()
}
}

/// Final teardown, called from HybridRiveView.dispose() (always on main).
/// Unlike the reload-path cleanup(), this also settles any pending
/// awaitViewReady() callers so their promises (which retain this view)
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19439,6 +19439,7 @@ __metadata:
"@react-native/metro-config": 0.80.3
"@react-native/typescript-config": 0.80.3
"@react-navigation/native": ^7.1.9
"@react-navigation/native-stack": ^7.3.16
"@react-navigation/stack": ^7.3.2
"@types/deep-equal": ^1.0.4
"@types/react": ^19.0.0
Expand Down
Loading