Skip to content

Commit 17dba4b

Browse files
authored
fix(iOS): fix quick modal presentation sequence where foreign modal is being dismissed (#2671)
## Description Closes #2668 See the #2668 for issue description. Making long story short: The issue involves cases when we have two modals `A, b`, where `A` is owned and `b` is foreign, and we dismiss `b` and present another owned modal `B` in single transaction or in separate transactions but pulled quickly enough so that previous transitions (caused by previous transaction) are not finished yet. Currently, because of `b` being dismissed by its host view controller, we would wrongfully dismiss the controller underneath (`A`). Now the code takes possibility of `b` being in dismissal into account. ## Changes We now check whether the potentially foreign modal is being dismissed - if so, we schedule our updates to run **after** dismissal transition completes. ## Test code and steps to reproduce Tested for regressions: * [x] `Modal` screen in Example [PR](#1996) * [x] Test2048 [PR](#2113) * [x] Test1829 [PR](#1912) * [x] Test1299 [PR](#1326) This issue is quite similar to #1299 but concerns foreign modal not owned one and mechanism are a bit different, therefore I've added `Test2668`. ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes
1 parent 51a1488 commit 17dba4b

3 files changed

Lines changed: 76 additions & 1 deletion

File tree

apps/src/tests/Test2668.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
2+
import { NativeStackNavigationProp, createNativeStackNavigator } from '@react-navigation/native-stack';
3+
import React from 'react';
4+
import { Button, Modal, Text, View } from 'react-native';
5+
import { SafeAreaView } from 'react-native-safe-area-context';
6+
7+
const Stack = createNativeStackNavigator();
8+
9+
function Home({ navigation }: { navigation: NativeStackNavigationProp<ParamListBase> }) {
10+
return (
11+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
12+
<Text>1. Open owned modal</Text>
13+
<Button title="Show modal" onPress={() => navigation.navigate('Modal')} />
14+
</View>
15+
);
16+
}
17+
18+
function Second({ navigation }: { navigation: NativeStackNavigationProp<ParamListBase> }) {
19+
return (
20+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
21+
<Text>4. This should be presented modally without any glitches</Text>
22+
<Button title="PopTo Home" onPress={() => {
23+
navigation.popTo('Home');
24+
}} />
25+
</View>
26+
);
27+
}
28+
29+
function ModalScreen({ navigation }: { navigation: NativeStackNavigationProp<ParamListBase> }) {
30+
const [modalVisible, setModalVisible] = React.useState(false);
31+
return (
32+
<View style={{ flex: 1 }}>
33+
<Text>2. Click "ShowModal" to open foreign modal</Text>
34+
<Button title="Show modal" onPress={() => setModalVisible(true)} />
35+
<Button title="PopTo Second" onPress={() => {
36+
//setModalVisible(false);
37+
//navigation.popTo('Second');
38+
navigation.navigate('Second');
39+
}} />
40+
<Modal visible={modalVisible} transparent={true} animationType="fade">
41+
<View style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center' }}>
42+
<Text>3. Dismiss foreign modal and present owned one in single transaction</Text>
43+
<Button title="Hide modal and navigate" onPress={() => {
44+
setModalVisible(false);
45+
navigation.navigate('Second');
46+
}} />
47+
</View>
48+
</Modal>
49+
</View>
50+
);
51+
}
52+
53+
export default function App() {
54+
return (
55+
<NavigationContainer>
56+
<Stack.Navigator>
57+
<Stack.Screen name="Home" component={Home} />
58+
<Stack.Screen name="Modal" component={ModalScreen} options={{ presentation: 'modal', headerShown: false }} />
59+
<Stack.Screen name="Second" component={Second} options={{ headerShown: false }} />
60+
</Stack.Navigator>
61+
</NavigationContainer>
62+
);
63+
}

apps/src/tests/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export { default as Test2395 } from './Test2395';
120120
export { default as Test2466 } from './Test2466';
121121
export { default as Test2552 } from './Test2552';
122122
export { default as Test2631 } from './Test2631';
123+
export { default as Test2668 } from './Test2668';
123124
export { default as TestScreenAnimation } from './TestScreenAnimation';
124125
export { default as TestScreenAnimationV5 } from './TestScreenAnimationV5';
125126
export { default as TestHeader } from './TestHeader';

ios/RNSScreenStack.mm

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,18 @@ - (void)setModalViewControllers:(NSArray<UIViewController *> *)controllers
552552
// See: https://github.com/software-mansion/react-native-screens/issues/2048
553553
// For now, to mitigate the issue, we also decide to trigger its dismissal before
554554
// starting the presentation chain down below in finish() callback.
555-
[changeRootController dismissViewControllerAnimated:shouldAnimate completion:finish];
555+
if (!firstModalToBeDismissed.isBeingDismissed) {
556+
[changeRootController dismissViewControllerAnimated:shouldAnimate completion:finish];
557+
} else {
558+
// We need to wait for its dismissal and then run our presentation code.
559+
// This happens, e.g. when we have foreign modal presented on top of owned one & we dismiss foreign one and
560+
// immediately present another owned one. Dismissal of the foreign one will be triggered by foreign controller.
561+
[[firstModalToBeDismissed transitionCoordinator]
562+
animateAlongsideTransition:nil
563+
completion:^(id<UIViewControllerTransitionCoordinatorContext> _) {
564+
finish();
565+
}];
566+
}
556567
return;
557568
}
558569
}

0 commit comments

Comments
 (0)