Skip to content

Commit

Permalink
fix(Paper,iOS): dismiss all attached view controllers correctly on re…
Browse files Browse the repository at this point in the history
…load (#2175)

## Description

This PR addresses an issue with react-native-screens where modals were
not being dismissed correctly during app reloads when combined with
foreign view controllers. The fix involves enhancing the invalidate
method to recursively dismiss all presented view controllers, ensuring a
clean state on reload.

This is not a development-only problem; this fix also addresses reloads
from OTA updates.

## Changes

- Enhanced invalidate method in RNSScreenStack.mm to recursively dismiss
all presented view controllers.
- Ensured _presentedModals is cleared and _controller is detached from
its parent during invalidation.
- Added a helper method dismissAllPresentedViewControllersFrom to handle
recursive dismissal logic.


## Screenshots / GIFs

| Before | After |
|--------|--------|
| <video width="320" height="240" controls
src="https://github.com/software-mansion/react-native-screens/assets/504909/1476ab3a-4bd9-4ffa-9256-760467a108bc"></video>
| <video width="320" height="240" controls
src="https://github.com/software-mansion/react-native-screens/assets/504909/e6c5eef0-16c6-49a2-9124-86467feec7f2"></video>
|

The red background is from a `transparentModal` by RNS, the sheet is a
foreign view controller. Before the change, `react-native-screens` would
break on reload if a foreign view controller was mounted on top. I came
to the solution after finding [this
PR](#2113).
The issue originally started
[here](lodev09/react-native-true-sheet#34).
After my changes, RNS works correctly on reload with third-party
controllers.

I have no experience with Fabric, so I can't help with that. Feel free
to update the solution for Fabric if needed.

## Test code and steps to reproduce

Test2175

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes

---------

Co-authored-by: Kacper Kafara <[email protected]>
Co-authored-by: adrianryt <[email protected]>
  • Loading branch information
3 people authored Oct 28, 2024
1 parent 437f6ea commit 88d9794
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 10 deletions.
151 changes: 151 additions & 0 deletions apps/src/tests/Test2175.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React from 'react';
import { Button, Modal, SafeAreaView, Text, View } from 'react-native';

import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Stack = createNativeStackNavigator();
const Tabs = createBottomTabNavigator();


function TabScreens({ navigation }): React.JSX.Element {
return (
<Tabs.Navigator>
<Tabs.Screen name='A' component={HomeScreen} />
<Tabs.Screen name='B' component={TabHomeScreen} />
</Tabs.Navigator>
);
}

function TabHomeScreen({ navigation }): React.JSX.Element {
return (
<View style={{ flex: 1, backgroundColor: 'lightcoral', justifyContent: 'center', }}>
<Text>Where do you where do you go, my lovely, oh oh oh oh</Text>
<Button title='Show owned transparentModal (outer navigator)' onPress={() => { navigation.navigate('TransparentModal') }} />
<Button title='Show owned modal (outer navigator)' onPress={() => { navigation.navigate('Modal') }} />
</View>
)
}


function HomeScreen({ navigation }): React.JSX.Element {
const [toggle, setToggle] = React.useState(false);
return (
<View style={{ flex: 1, backgroundColor: 'lightseagreen', justifyContent: 'center', }}>
<Text>Where do you where do you go, my lovely, oh oh oh oh</Text>
<Button title='Show owned transparentModal' onPress={() => { navigation.navigate('TransparentModal') }} />
<Button title='Show owned modal' onPress={() => { navigation.navigate('Modal') }} />
<Button title='Show tabs' onPress={() => { navigation.navigate('Tabs') }} />
<Button title='Show foreign modal' onPress={() => { setToggle(old => !old) }} />
<Modal
visible={toggle}
onRequestClose={() => setToggle(false)}
presentationStyle='formSheet'
animationType='slide'
>
<View style={{ flex: 1, backgroundColor: 'lightblue' }}>
<Text>Hello I'm a foreign modal</Text>
<Button title='Close foreign modal' onPress={() => { setToggle(false) }} />
</View>
</Modal>
</View>
)
}

function ModalScreen({ navigation }): React.JSX.Element {
const [toggle2, setToggle2] = React.useState(false);

return (
<View style={{ flex: 1, backgroundColor: 'lightcoral', opacity: 0.4 }}>
<Text>Where do you where do you go, my lovely, oh oh oh oh</Text>
<Button title='Go back' onPress={() => { navigation.goBack() }} />
<View style={{ width: '100%', height: 50, backgroundColor: 'red' }} />
<Button title='Push another Modal' onPress={() => { navigation.push('Modal') }} />
<Button title='Push foreign modal(inside Screen Component)' onPress={() => { navigation.push('ForeignModal')}} />
<Button title='Push foreign modal' onPress={() => { setToggle2(old => !old )}} />
<Modal
visible={toggle2}
onRequestClose={() => setToggle2(false)}
presentationStyle='formSheet'
animationType='slide'
>
<View style={{ flex: 1, backgroundColor: 'lightblue' }}>
<Text>Hello I'm a foreign modal</Text>
<Button title='Close foreign modal' onPress={() => { setToggle2(false) }} />
</View>
</Modal>
</View>
)
}

function ForeignModal({ navigation }): React.JSX.Element | null {
const [toggle, setToggle] = React.useState(false);
return (
<Modal
visible={toggle}
onRequestClose={() => setToggle(false)}
presentationStyle='formSheet'
animationType='slide'
>
<View style={{ flex: 1, backgroundColor: 'lightblue' }}>
<Text>Hello I'm a foreign modal</Text>
<Button title='Close foreign modal' onPress={() => { setToggle(false) }} />
</View>
</Modal>
)
}


const TestScreen = ({ navigation }): React.JSX.Element => {
return (
<SafeAreaView>
<Button
title={
'Click me and drag around a bit and I should log something still'
}
onPress={() => {
console.log(Date.now());
}}
/>
<Button
title={'Navigate to modal'}
onPress={() => {
navigation.navigate('Test2');
}}
/>
</SafeAreaView>
);
};

function App(): React.JSX.Element {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName='Tabs'>
<Stack.Screen name='Tabs' component={TabScreens} options={{ headerShown: false }} />
<Stack.Screen
name="Home"
component={HomeScreen}
/>
<Stack.Screen
name="TransparentModal"
component={ModalScreen}
options={{
presentation: 'transparentModal',
}}
/>
<Stack.Screen
name="Modal"
component={ModalScreen}
options={{
presentation: 'modal',
headerShown: true,
}}
/>
<Stack.Screen name={"ForeignModal"} component={ForeignModal} />
</Stack.Navigator>
</NavigationContainer>
);
}

export default App;
1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export { default as Test2028 } from './Test2028';
export { default as Test2048 } from './Test2048';
export { default as Test2069 } from './Test2069';
export { default as Test2118 } from './Test2118';
export { default as Test2175 } from './Test2175';
export { default as Test2184 } from './Test2184';
export { default as Test2223 } from './Test2223';
export { default as Test2227 } from './Test2227';
Expand Down
19 changes: 16 additions & 3 deletions ios/RNSScreenStack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1273,12 +1273,25 @@ - (void)prepareForRecycle
- (void)invalidate
{
_invalidated = YES;
for (UIViewController *controller in _presentedModals) {
[self dismissAllRelatedModals];
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
}

// This method aims to dismiss all modals for which presentation process
// has been initiated in this navigation controller, i. e. either a Screen
// with modal presentation or foreign modal presented from inside a Screen.
- (void)dismissAllRelatedModals
{
[_controller dismissViewControllerAnimated:NO completion:nil];

// This loop seems to be excessive. Above message send to `_controller` should
// be enough, because system dismisses the controllers recursively,
// however better safe than sorry & introduce a regression, thus it is left here.
for (UIViewController *controller in [_presentedModals reverseObjectEnumerator]) {
[controller dismissViewControllerAnimated:NO completion:nil];
}
[_presentedModals removeAllObjects];
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
}

#endif // RCT_NEW_ARCH_ENABLED
Expand Down
11 changes: 4 additions & 7 deletions src/components/Screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,15 +373,12 @@ export const InnerScreen = React.forwardRef<View, ScreenProps>(
// e.g. to use `useReanimatedTransitionProgress` (see `reanimated` folder in repo)
export const ScreenContext = React.createContext(InnerScreen);

const Screen = React.forwardRef<View, ScreenProps>(
(props, ref) => {
const ScreenWrapper = React.useContext(ScreenContext) || InnerScreen;
const Screen = React.forwardRef<View, ScreenProps>((props, ref) => {
const ScreenWrapper = React.useContext(ScreenContext) || InnerScreen;

return <ScreenWrapper {...props} ref={ref} />;
}
);
return <ScreenWrapper {...props} ref={ref} />;
});

Screen.displayName = 'Screen';


export default Screen;

0 comments on commit 88d9794

Please sign in to comment.