Skip to content

Commit

Permalink
fix(iOS): app freeze when navigating back from any modal nested in co…
Browse files Browse the repository at this point in the history
…ntained modal (#1996)

## Description

Updated logic in `RNSScreenStackView#setModalViewControllers:` so that
it handles case when navigating back from top-level VC that was not
presented
by the screen stack itself, as it happens with
`UIModalPresentationCurrentContext` &
`UIModalPresentationOverCurrentContext`.

Fixes #1813

## Changes

We now check whether the top-level VC should be dismissed by the stack
being asked.

## Test code and steps to reproduce

See #1813 for reproduction steps.

I'm also adding these in `Example` application.

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes
  • Loading branch information
kkafar authored Jan 2, 2024
1 parent 28e4b9a commit 8969d3b
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 3 deletions.
14 changes: 14 additions & 0 deletions Example/src/screens/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type StackParamList = {
Modal: undefined;
FullscreenModal: undefined;
Alert: undefined;
ContainedModal: undefined;
};

interface MainScreenProps {
Expand All @@ -25,6 +26,10 @@ const MainScreen = ({ navigation }: MainScreenProps): JSX.Element => (
onPress={() => navigation.navigate('FullscreenModal')}
/>
<Button title="Open alert" onPress={() => navigation.navigate('Alert')} />
<Button
title="Open contained modal"
onPress={() => navigation.navigate('ContainedModal')}
/>
<Button onPress={() => navigation.pop()} title="🔙 Back to Examples" />
</View>
);
Expand All @@ -41,6 +46,10 @@ const ModalScreen = ({ navigation }: ModalScreenProps): JSX.Element => (
onPress={() => navigation.push('FullscreenModal')}
/>
<Button title="Open alert" onPress={() => navigation.navigate('Alert')} />
<Button
title="Open contained modal"
onPress={() => navigation.navigate('ContainedModal')}
/>
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
);
Expand Down Expand Up @@ -68,6 +77,11 @@ const App = (): JSX.Element => (
component={ModalScreen}
options={{ stackPresentation: 'fullScreenModal' }}
/>
<Stack.Screen
name="ContainedModal"
component={ModalScreen}
options={{ stackPresentation: 'containedModal' }}
/>
<Stack.Screen
name="Alert"
component={Alert}
Expand Down
2 changes: 1 addition & 1 deletion TestsExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ import Test1683 from './src/Test1683';
import Test1726 from './src/Test1726';
import Test1791 from './src/Test1791';
import Test1802 from './src/Test1802';
import Test1829 from './src/Test1829';
import Test1844 from './src/Test1844';
import Test1864 from './src/Test1864';
import Test1829 from './src/Test1829';
import Test1981 from './src/Test1981';

enableFreeze(true);
Expand Down
16 changes: 14 additions & 2 deletions ios/RNSScreenStack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -501,15 +501,27 @@ - (void)setModalViewControllers:(NSArray<UIViewController *> *)controllers
}
}

// changeRootController does not have presentedViewController but it does not mean that no modals are in presentation,
// so we need to find top-level controller manually
// changeRootController does not have presentedViewController but it does not mean that no modals are in presentation;
// modals could be presented by another stack (nested / outer), third-party view controller or they could be using
// UIModalPresentationCurrentContext / UIModalPresentationOverCurrentContext presentation styles; in the last case
// for some reason system asks top-level (react root) vc to present instead of our stack, despite the fact that
// `definesPresentationContext` returns `YES` for UINavigationController.
// So we first need to find top-level controller manually:
UIViewController *reactRootVc = [self findReactRootViewController];
UIViewController *topMostVc = [RNSScreenStackView findTopMostPresentedViewControllerFromViewController:reactRootVc];

if (topMostVc != reactRootVc) {
changeRootController = topMostVc;

// Here we handle just the simplest case where the top level VC was dismissed. In any more complex
// scenario we will still have problems, see: https://github.com/software-mansion/react-native-screens/issues/1813
if ([_presentedModals containsObject:topMostVc] && ![controllers containsObject:topMostVc]) {
[changeRootController dismissViewControllerAnimated:YES completion:finish];
return;
}
}

// We didn't detect any controllers for dismissal, thus we start presenting new VCs
finish();
}

Expand Down

0 comments on commit 8969d3b

Please sign in to comment.