Skip to content

Commit 4d044da

Browse files
authored
feat(iOS): add pageSheet presentation for native-stack (#2793)
## Description Adds new presentation for Native Stack - `pageSheet` (`UIModalPresentationPageSheet` modal style on iOS, `modal` on Android). Since the release of iOS 18, screens with `stackPresentation: 'modal'` changed size on devices with a bigger screen, such as an iPad - they became much smaller, which impacted applications of the library's users (see issue #2549). This happens because `UIModalPresentationAutomatic`, which is used by `react-native-screens` for `modal` on iOS, has been changed with the release of iOS 18. `UIModalPresentationAutomatic` is now mapped to `UIModalPresentationFormSheet` for iOS >=18 and `UIModalPresentationPageSheet` for earlier versions (this was the default before). ~~Apple added SwiftUI API [(link)](https://developer.apple.com/documentation/swiftui/presentationsizing) to allow to choose the behavior of the modal (`form`, `page`, `fitted` or a custom one) but I wasn't able to find an equivalent functionality in UIKit.~~ It turns out there is a new property in `UISheetPresentationController`, see [here](#2793 (review)) We considered 3 possible solutions to this problem: - changing modal presentation style on iOS from `UIModalPresentationAutomatic` to `UIModalPresentationPageSheet` for `stackPresentation: 'modal'` to bring back old behavior for all modals: - some users might've already designed their apps with the smaller sheet in mind so changing the style might break their apps, - adding a feature flag that would allow users to choose whether screens with `stackPresentation: 'modal'` should use `UIModalPresentationAutomatic` or `UIModalPresentationPageSheet` - this solution would provide the easiest fix for the users impacted by the change, - even though 2 different sheet sizes would be available, users would need to choose only one of them for their application, - adding a new `stackPresentation` that would use `UIModalPresentationPageSheet` on iOS and a regular `modal` on Android: - users can choose whether they want to use `Automatic` or `PageSheet` behavior per-screen basis, - hopefully, fixing old applications with some find-and-replace magic won't be difficult. We decided on the third option. tvOS does not allow `UIModalPresentationPageSheet` so it fallbacks to `UIModalPresentationFullScreen` (the same behavior as `UIModalPresentationAutomatic`). This change will require changes to `react-navigation/native-stack` as well. Resolves #2549. ## Changes - add support for the new presentation in native and JS code - add `pageSheet` examples in `Stack Presentation` and `Modals` example screens ## Screenshots / GIFs ### `modal` ![modal](https://github.com/user-attachments/assets/657d0130-dfa0-4433-8f11-d8eb32a6146b) ### `pageSheet` ![pageSheet](https://github.com/user-attachments/assets/4759b662-119a-4886-82c4-6712502d3b6a) ## Test code and steps to reproduce Open `Stack Presentation`, `Modals` example screens and open `pageSheet` (at the moment, you might need to manually add `pageSheet` in react-navigation files `packages/native-stack/src`, `packages/native-stack/src/utils/getModalRoutesKeys.ts`, `packages/native-stack/src/views`). ## Checklist - [x] Included code example that can be used to test this change - [x] Updated TS types - [x] Updated documentation: <!-- For adding new props to native-stack --> - [x] https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md - [x] https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md - [x] https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx - [x] https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx - [ ] Ensured that CI passes
1 parent cc73b18 commit 4d044da

13 files changed

Lines changed: 70 additions & 6 deletions

File tree

android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ open class ScreenViewManager :
126126
when (presentation) {
127127
"push" -> Screen.StackPresentation.PUSH
128128
"formSheet" -> Screen.StackPresentation.FORM_SHEET
129-
"modal", "containedModal", "fullScreenModal" ->
129+
"modal", "containedModal", "fullScreenModal", "pageSheet" ->
130130
Screen.StackPresentation.MODAL
131131
"transparentModal", "containedTransparentModal" ->
132132
Screen.StackPresentation.TRANSPARENT_MODAL

apps/src/screens/Modals.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type StackParamList = {
1212
FullscreenModal: undefined;
1313
Alert: undefined;
1414
ContainedModal: undefined;
15+
PageSheet: undefined;
1516
};
1617

1718
interface MainScreenProps {
@@ -30,6 +31,10 @@ const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => (
3031
title="Open contained modal"
3132
onPress={() => navigation.navigate('ContainedModal')}
3233
/>
34+
<Button
35+
title="Open pageSheet"
36+
onPress={() => navigation.navigate('PageSheet')}
37+
/>
3338
<Button onPress={() => navigation.pop()} title="🔙 Back to Examples" />
3439
</View>
3540
);
@@ -50,6 +55,10 @@ const ModalScreen = ({ navigation }: ModalScreenProps): React.JSX.Element => (
5055
title="Open contained modal"
5156
onPress={() => navigation.navigate('ContainedModal')}
5257
/>
58+
<Button
59+
title="Open pageSheet"
60+
onPress={() => navigation.push('PageSheet')}
61+
/>
5362
<Button title="Go back" onPress={() => navigation.goBack()} />
5463
</View>
5564
);
@@ -81,6 +90,11 @@ const App = (): React.JSX.Element => (
8190
component={ModalScreen}
8291
options={{ presentation: 'containedModal' }}
8392
/>
93+
<Stack.Screen
94+
name="PageSheet"
95+
component={ModalScreen}
96+
options={{ presentation: 'pageSheet' }}
97+
/>
8498
<Stack.Screen
8599
name="Alert"
86100
component={Alert}

apps/src/screens/StackPresentation.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type StackParamList = {
1616
ContainedTransparentModal: undefined;
1717
FullScreenModal: undefined;
1818
FormSheet: { usesFormSheetPresentation?: boolean };
19+
PageSheet: undefined;
1920
};
2021

2122
interface MainScreenProps {
@@ -62,6 +63,11 @@ const MainScreen = ({ navigation }: MainScreenProps): React.JSX.Element => {
6263
onPress={() => navigation.navigate('FormSheet')}
6364
testID="stack-presentation-form-sheet-button"
6465
/>
66+
<Button
67+
title="pageSheet"
68+
onPress={() => navigation.navigate('PageSheet')}
69+
testID="stack-presentation-page-sheet-button"
70+
/>
6571
<Button
6672
testID="stack-presentation-go-back-button"
6773
onPress={() => navigation.pop()}
@@ -123,6 +129,22 @@ const ModalScreen = ({ navigation }: ModalScreenProps): React.JSX.Element => (
123129
</View>
124130
);
125131

132+
interface PageSheetScreenProps {
133+
navigation: NativeStackNavigationProp<ParamListBase>;
134+
}
135+
136+
const PageSheetScreen = ({ navigation }: PageSheetScreenProps): React.JSX.Element => (
137+
<View style={styles.container}>
138+
<Choose />
139+
<Button
140+
testID="stack-presentation-page-sheet-screen-go-back-button"
141+
title="Go back"
142+
onPress={() => navigation.goBack()}
143+
/>
144+
</View>
145+
);
146+
147+
126148
interface FullScreenModalProps {
127149
navigation: NativeStackNavigationProp<ParamListBase>;
128150
}
@@ -204,6 +226,11 @@ const App = (): React.JSX.Element => (
204226
usesFormSheetPresentation: true
205227
}}
206228
/>
229+
<Stack.Screen
230+
name="PageSheet"
231+
component={PageSheetScreen}
232+
options={{ presentation: 'pageSheet' }}
233+
/>
207234
</Stack.Navigator>
208235
);
209236

guides/GUIDE_FOR_LIBRARY_AUTHORS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ Defines how the method that should be used to present the given screen. It is a
242242
- `containedTransparentModal` – Explained below.
243243
- `fullScreenModal` – Explained below.
244244
- `formSheet` – Explained below.
245+
- `pageSheet` - Explained below.
245246

246247
Using `containedModal` and `containedTransparentModal` with other types of modals in one native stack navigator is not recommended and can result in a freeze or a crash of the application.
247248

@@ -253,13 +254,14 @@ For iOS:
253254
* on iOS 12 and earlier will use [`UIModalPresentationFullScreen`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationfullscreen?language=objc).
254255
- `fullScreenModal` will use [`UIModalPresentationFullScreen`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationfullscreen?language=objc)
255256
- `formSheet` will use [`UIModalPresentationFormSheet`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationformsheet?language=objc)
257+
- `pageSheet` will use [`UIModalPresentationPageSheet`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/pagesheet?language=objc)
256258
- `transparentModal` will use [`UIModalPresentationOverFullScreen`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationoverfullscreen?language=objc)
257259
- `containedModal` will use [`UIModalPresentationCurrentContext`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationcurrentcontext?language=objc)
258260
- `containedTransparentModal` will use [`UIModalPresentationOverCurrentContext`](https://developer.apple.com/documentation/uikit/uimodalpresentationstyle/uimodalpresentationovercurrentcontext?language=objc)
259261

260262
For Android:
261263

262-
`modal`, `containedModal`, `fullScreenModal`, `formSheet` will use `Screen.StackPresentation.MODAL`.
264+
`modal`, `containedModal`, `fullScreenModal`, `formSheet`, `pageSheet` will use `Screen.StackPresentation.MODAL`.
263265

264266
`transparentModal`, `containedTransparentModal` will use `Screen.StackPresentation.TRANSPARENT_MODAL`.
265267

ios/RNSConvert.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ + (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent:
4949
return RNSScreenStackPresentationFullScreenModal;
5050
case FormSheet:
5151
return RNSScreenStackPresentationFormSheet;
52+
case PageSheet:
53+
return RNSScreenStackPresentationPageSheet;
5254
case ContainedModal:
5355
return RNSScreenStackPresentationContainedModal;
5456
case TransparentModal:

ios/RNSEnums.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ typedef NS_ENUM(NSInteger, RNSScreenStackPresentation) {
55
RNSScreenStackPresentationContainedModal,
66
RNSScreenStackPresentationContainedTransparentModal,
77
RNSScreenStackPresentationFullScreenModal,
8-
RNSScreenStackPresentationFormSheet
8+
RNSScreenStackPresentationFormSheet,
9+
RNSScreenStackPresentationPageSheet,
910
};
1011

1112
typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {

ios/RNSScreen.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,15 @@ - (void)setStackPresentation:(RNSScreenStackPresentation)stackPresentation
260260
}
261261
#endif
262262
break;
263+
264+
case RNSScreenStackPresentationPageSheet:
265+
#if !TARGET_OS_TV
266+
_controller.modalPresentationStyle = UIModalPresentationPageSheet;
267+
#else
268+
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
269+
#endif
270+
break;
271+
263272
case RNSScreenStackPresentationFullScreenModal:
264273
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
265274
break;
@@ -2036,6 +2045,7 @@ @implementation RCTConvert (RNSScreen)
20362045
@"modal" : @(RNSScreenStackPresentationModal),
20372046
@"fullScreenModal" : @(RNSScreenStackPresentationFullScreenModal),
20382047
@"formSheet" : @(RNSScreenStackPresentationFormSheet),
2048+
@"pageSheet" : @(RNSScreenStackPresentationPageSheet),
20392049
@"containedModal" : @(RNSScreenStackPresentationContainedModal),
20402050
@"transparentModal" : @(RNSScreenStackPresentationTransparentModal),
20412051
@"containedTransparentModal" : @(RNSScreenStackPresentationContainedTransparentModal)

native-stack/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ How the screen should be presented. Possible values:
350350
- `containedTransparentModal` – will use "UIModalPresentationOverCurrentContext" modal style on iOS and will fallback to `"transparentModal"` on Android.
351351
- `fullScreenModal` – will use "UIModalPresentationFullScreen" modal style on iOS and will fallback to `"modal"` on Android.
352352
- `formSheet` – will use "UIModalPresentationFormSheet" modal style on iOS and will fallback to `"modal"` on Android.
353+
- `pageSheet` – will use "UIModalPresentationPageSheet" modal style on iOS and will fallback to `"modal"` on Android.
353354

354355
Defaults to `push`.
355356

src/fabric/ModalScreenNativeComponent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type StackPresentation =
4545
| 'transparentModal'
4646
| 'fullScreenModal'
4747
| 'formSheet'
48+
| 'pageSheet'
4849
| 'containedModal'
4950
| 'containedTransparentModal';
5051

src/fabric/ScreenNativeComponent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type StackPresentation =
4545
| 'transparentModal'
4646
| 'fullScreenModal'
4747
| 'formSheet'
48+
| 'pageSheet'
4849
| 'containedModal'
4950
| 'containedTransparentModal';
5051

0 commit comments

Comments
 (0)