diff --git a/FabricTestExample/App.js b/FabricTestExample/App.js
index 1b50c3eff6..2aea269a4d 100644
--- a/FabricTestExample/App.js
+++ b/FabricTestExample/App.js
@@ -94,6 +94,7 @@ import TestScreenAnimation from './src/TestScreenAnimation';
import Test1981 from './src/Test1981';
import Test2008 from './src/Test2008';
import Test2028 from './src/Test2028';
+import Test2048 from './src/Test2048';
import Test2069 from './src/Test2069';
enableFreeze(true);
diff --git a/FabricTestExample/src/Test2048.tsx b/FabricTestExample/src/Test2048.tsx
new file mode 100644
index 0000000000..e4efdcaf3b
--- /dev/null
+++ b/FabricTestExample/src/Test2048.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { View, Modal, Button, TouchableWithoutFeedback } from 'react-native';
+import { useState } from 'react';
+
+import { NavigationContainer, useNavigation } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+
+type AppStackPages = {
+ Home: undefined;
+ Modal: undefined;
+};
+
+function HomeScreen() {
+ const navigation = useNavigation();
+ const [visible, setVisible] = useState(false);
+
+ return (
+
+
+ );
+}
+
+function ModalScreen() {
+ return ;
+}
+
+const AppStack = createNativeStackNavigator();
+
+function Navigation() {
+ return (
+
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/TestsExample/App.js b/TestsExample/App.js
index b34e9d99f1..73b1f6eac2 100644
--- a/TestsExample/App.js
+++ b/TestsExample/App.js
@@ -95,6 +95,7 @@ import Test1844 from './src/Test1844';
import Test1864 from './src/Test1864';
import Test1981 from './src/Test1981';
import Test2008 from './src/Test2008';
+import Test2048 from './src/Test2048';
import Test2069 from './src/Test2069';
enableFreeze(true);
diff --git a/TestsExample/src/Test2048.tsx b/TestsExample/src/Test2048.tsx
new file mode 100644
index 0000000000..e4efdcaf3b
--- /dev/null
+++ b/TestsExample/src/Test2048.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { View, Modal, Button, TouchableWithoutFeedback } from 'react-native';
+import { useState } from 'react';
+
+import { NavigationContainer, useNavigation } from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+
+type AppStackPages = {
+ Home: undefined;
+ Modal: undefined;
+};
+
+function HomeScreen() {
+ const navigation = useNavigation();
+ const [visible, setVisible] = useState(false);
+
+ return (
+
+ setVisible(prev => !prev)}
+ />
+
+ setVisible(false)}>
+
+
+
+ {
+ // Issue: autohiding the Modal that serves as a bottom sheet unmounts
+ // the anchor component for the screen that is in { presentation: "modal" } mode
+ // Previously the anchoring component for a { presentation: "modal" }-based screen was different and it worked
+ // The culprit is: https://github.com/software-mansion/react-native-screens/pull/1912 released in https://github.com/software-mansion/react-native-screens/releases/tag/3.29.0
+ // adding setTimeout does not bring any good, because
+ // - we either don't see navigation action
+ // - we unmount both the bottom sheet modal and the screen itself
+
+ setVisible(false);
+
+ navigation.navigate('Modal');
+ }}
+ />
+
+
+
+ );
+}
+
+function ModalScreen() {
+ return ;
+}
+
+const AppStack = createNativeStackNavigator();
+
+function Navigation() {
+ return (
+
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm
index b96bb9ed52..7aa23908a8 100644
--- a/ios/RNSScreenStack.mm
+++ b/ios/RNSScreenStack.mm
@@ -382,9 +382,12 @@ - (void)setModalViewControllers:(NSArray *)controllers
[newControllers removeObjectsInArray:_presentedModals];
// We need to find bottom-most view controller that should stay on the stack
- // for the duration of transition. There are couple of scenarios:
- // (1) No modals are presented or all modals were presented by this RNSNavigationController,
- // (2) There are modals presented by other RNSNavigationControllers (nested/outer)
+ // for the duration of transition.
+
+ // There are couple of scenarios:
+ // (1) no modals are presented or all modals were presented by this RNSNavigationController,
+ // (2) there are modals presented by other RNSNavigationControllers (nested/outer),
+ // (3) there are modals presented by other controllers (e.g. React Native's Modal view).
// Last controller that is common for both _presentedModals & controllers
__block UIViewController *changeRootController = _controller;
@@ -479,16 +482,35 @@ - (void)setModalViewControllers:(NSArray *)controllers
}
};
+ // changeRootController is the last controller that *is owned by this stack*, and should stay unchanged after this
+ // batch of transitions. Therefore changeRootController.presentedViewController is the first constroller to be
+ // dismissed (implying also all controllers above). Notice here, that firstModalToBeDismissed could have been
+ // RNSScreen modal presented from *this* stack, another stack, or any other view controller with modal presentation
+ // provided by third-party libraries (e.g. React Native's Modal view). In case of presence of other (not managed by
+ // us) modal controllers, weird interactions might arise. The code below, besides handling our presentation /
+ // dismissal logic also attempts to handle possible wide gamut of cases of interactions with third-party modal
+ // controllers, however it's not perfect.
+ // TODO: Find general way to manage owned and foreign modal view controllers and refactor this code. Consider building
+ // model first (data structue, attempting to be aware of all modals in presentation and some text-like algorithm for
+ // computing required operations).
+
UIViewController *firstModalToBeDismissed = changeRootController.presentedViewController;
+
if (firstModalToBeDismissed != nil) {
BOOL shouldAnimate = changeRootIndex == controllers.count &&
[firstModalToBeDismissed isKindOfClass:[RNSScreen class]] &&
((RNSScreen *)firstModalToBeDismissed).screenView.stackAnimation != RNSScreenStackAnimationNone;
- if ([_presentedModals containsObject:firstModalToBeDismissed]) {
+ if ([_presentedModals containsObject:firstModalToBeDismissed] ||
+ ![firstModalToBeDismissed isKindOfClass:RNSScreen.class]) {
// We dismiss every VC that was presented by changeRootController VC or its descendant.
// After the series of dismissals is completed we run completion block in which
// we present modals on top of changeRootController (which may be the this stack VC)
+ //
+ // There also might the second case, where the firstModalToBeDismissed is foreign.
+ // See: https://github.com/software-mansion/react-native-screens/issues/2048
+ // For now, to mitigate the issue, we also decide to trigger its dismissal before
+ // starting the presentation chain down below in finish() callback.
[changeRootController dismissViewControllerAnimated:shouldAnimate completion:finish];
return;
}