Skip to content

fix(iOS): replace force unwrapping with safe casting in RiveRNNativeViewMgr#381

Merged
mfazekas merged 2 commits intorive-app:mainfrom
mfazekas:fix/376-safe-view-casting
Oct 31, 2025
Merged

fix(iOS): replace force unwrapping with safe casting in RiveRNNativeViewMgr#381
mfazekas merged 2 commits intorive-app:mainfrom
mfazekas:fix/376-safe-view-casting

Conversation

@mfazekas
Copy link
Copy Markdown
Collaborator

@mfazekas mfazekas commented Oct 29, 2025

Fixes #376, #378

Previously, all view manager methods used force unwrapping (as!) which caused crashes when views were nil or not the correct type. This resulted in "Unexpectedly found nil while unwrapping an Optional value" errors.

Changes:

  • Added withRiveReactNativeView helper function that safely handles:
    • Bridge nil checks
    • View existence checks
    • Type casting with proper error logging via RCTLogError
  • Refactored all 18 view manager methods to use the helper
  • Fixed double optional bug in registerPropertyListener
  • Replaced force unwrap (try!) with proper error handling in text run methods
  • All errors now log descriptive messages instead of crashing

This ensures the app logs errors gracefully instead of crashing when views are accessed after being deallocated or when view tags are invalid.

…wManager

Fixes rive-app#376

Previously, all view manager methods used force unwrapping (as!) which caused crashes when views were nil or not the correct type. This resulted in "Unexpectedly found nil while unwrapping an Optional value" errors.

Changes:
- Added `withRiveReactNativeView` helper function that safely handles:
  - Bridge nil checks
  - View existence checks
  - Type casting with proper error logging via RCTLogError
- Refactored all 18 view manager methods to use the helper
- Fixed double optional bug in registerPropertyListener
- Replaced force unwrap (try!) with proper error handling in text run methods
- All errors now log descriptive messages instead of crashing

This ensures the app logs errors gracefully instead of crashing when views are accessed after being deallocated or when view tags are invalid.
@mfazekas mfazekas changed the title fix: replace force unwrapping with safe casting in RiveRNNativeViewMgr fix(iOS): replace force unwrapping with safe casting in RiveRNNativeViewMgr Oct 29, 2025
@mfazekas mfazekas marked this pull request as ready for review October 29, 2025 21:21
@mfazekas mfazekas requested a review from HayesGordon October 29, 2025 21:36
HayesGordon
HayesGordon previously approved these changes Oct 30, 2025
Copy link
Copy Markdown
Contributor

@HayesGordon HayesGordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@mfazekas
Copy link
Copy Markdown
Collaborator Author

So I wasn't able to fully reproduce the issue as is but with those tweaks:

replaced the main.async with a 100ms delay, which is probably possible if we're unlucky, and things are busy.

   DispatchQueue.main.async {
        let delayMs: Int = 100
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delayMs)) {

Added a component to our test suite that shows a modal when closing it calls setBoolean just after closing.

And I got this error, with the fixed version:

image
import React, { useRef, useState } from 'react';
import {
 SafeAreaView,
 ScrollView,
 StyleSheet,
 View,
 Text,
 Modal,
} from 'react-native';
import Rive, { Alignment, Fit, RiveRef } from 'rive-react-native';
import { Button } from 'react-native-paper';

export default function Issue378Modal() {
 const [modalVisible, setModalVisible] = useState(false);
 const riveRef = useRef<RiveRef>(null);

 const handleCloseModal = () => {
   setModalVisible(false);

   // Call setBoolean at various delays after closing
   const delays = [100, 200, 300, 400, 500];
   delays.forEach((delay) => {
     setTimeout(() => {
       riveRef.current?.setBoolean('someProperty', true);
     }, delay);
   });
 };

 return (
   <SafeAreaView style={styles.safeAreaViewContainer}>
     <ScrollView contentContainerStyle={styles.container}>
       <Text style={styles.title}>Issue #378 - Modal Test</Text>
       <Text style={styles.description}>
         Close modal and call setBoolean on deallocated view to test issue #378
         fix.
       </Text>

       <Button
         mode="contained"
         onPress={() => setModalVisible(true)}
         style={styles.button}
       >
         Open Modal
       </Button>

       <Modal
         visible={modalVisible}
         animationType="slide"
         transparent={true}
         onRequestClose={handleCloseModal}
       >
         <View style={styles.modalOverlay}>
           <View style={styles.modalContent}>
             <Text style={styles.modalTitle}>Rive Animation in Modal</Text>
             <Rive
               ref={riveRef}
               fit={Fit.Contain}
               alignment={Alignment.Center}
               style={styles.animation}
               artboardName={'Avatar 3'}
               autoplay={true}
               resourceName={'avatars'}
             />
           </View>
           <Button
             mode="contained"
             onPress={handleCloseModal}
             style={styles.closeButton}
           >
             Close Modal (Trigger Test)
           </Button>
         </View>
       </Modal>
     </ScrollView>
   </SafeAreaView>
 );
}

const styles = StyleSheet.create({
 safeAreaViewContainer: {
   flex: 1,
 },
 container: {
   flexGrow: 1,
   alignItems: 'center',
   justifyContent: 'center',
   padding: 16,
 },
 title: {
   fontSize: 20,
   fontWeight: 'bold',
   marginBottom: 8,
   textAlign: 'center',
 },
 description: {
   fontSize: 14,
   marginBottom: 16,
   textAlign: 'center',
   color: '#666',
 },
 button: {
   marginVertical: 10,
 },
 modalOverlay: {
   flex: 1,
   backgroundColor: 'rgba(0, 0, 0, 0.5)',
   justifyContent: 'center',
   alignItems: 'center',
   padding: 20,
 },
 modalContent: {
   backgroundColor: 'white',
   borderRadius: 10,
   padding: 20,
   alignItems: 'center',
   minWidth: 300,
 },
 modalTitle: {
   fontSize: 18,
   fontWeight: 'bold',
   marginBottom: 16,
 },
 animation: {
   width: 250,
   height: 250,
   marginVertical: 20,
 },
 closeButton: {
   marginTop: 20,
 },
});

@mfazekas mfazekas merged commit d434b05 into rive-app:main Oct 31, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unexpectedly found nil while unwrapping an Optional value

2 participants