Skip to content

Commit 28986a7

Browse files
okwasniewskifacebook-github-bot
authored andcommitted
fix(iOS): allow to interactively swipe down the modal (#51483)
Summary: This PR allows to interactively close the modal using the swipe down gesture. It fixes 5 year old issue: #29319 In short it removes `modalInPresentation` which according to the documentation causes: "UIKit ignores events outside the view controller’s bounds and **prevents the interactive dismissal of the view controller while it is onscreen.**". It also adds another delegate event to call onRequestClose whenever modal is closed by gesture. https://github.com/user-attachments/assets/8849ecba-f762-47ec-a28b-b41c1991a882 ## Changelog: [IOS] [ADDED] - Allow to interactively swipe down the modal. Add allowSwipeDismissal prop. Pull Request resolved: #51483 Test Plan: Test if swiping down the modal calls onRequestClose Reviewed By: rshest Differential Revision: D75125438 Pulled By: javache fbshipit-source-id: d4f2c8b59447680f405b725d0809573a937f97cf
1 parent ce75271 commit 28986a7

13 files changed

Lines changed: 90 additions & 8 deletions

File tree

packages/react-native/Libraries/Modal/Modal.d.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ export interface ModalBaseProps {
3535
*/
3636
visible?: boolean | undefined;
3737
/**
38-
* The `onRequestClose` callback is called when the user taps the hardware back button on Android or the menu button on Apple TV.
38+
* The `onRequestClose` callback is called when the user taps the hardware back button on Android, dismisses the sheet using a gesture on iOS (when `allowSwipeDismissal` is set to true) or the menu button on Apple TV.
3939
*
40-
* This is required on Apple TV and Android.
40+
* This is required on iOS and Android.
4141
*/
4242
onRequestClose?: ((event: NativeSyntheticEvent<any>) => void) | undefined;
4343
/**
@@ -89,6 +89,12 @@ export interface ModalPropsIOS {
8989
onOrientationChange?:
9090
| ((event: NativeSyntheticEvent<any>) => void)
9191
| undefined;
92+
93+
/**
94+
* Controls whether the modal can be dismissed by swiping down on iOS.
95+
* This requires you to implement the `onRequestClose` prop to handle the dismissal.
96+
*/
97+
allowSwipeDismissal?: boolean | undefined;
9298
}
9399

94100
export interface ModalPropsAndroid {

packages/react-native/Libraries/Modal/Modal.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ export type ModalBaseProps = {
8686
*/
8787
visible?: ?boolean,
8888
/**
89-
* The `onRequestClose` callback is called when the user taps the hardware back button on Android or the menu button on Apple TV.
89+
* The `onRequestClose` callback is called when the user taps the hardware back button on Android, dismisses the sheet using a gesture on iOS (when `allowSwipeDismissal` is set to true) or the menu button on Apple TV.
9090
*
91-
* This is required on Apple TV and Android.
91+
* This is required on iOS and Android.
9292
*/
9393
// onRequestClose?: (event: NativeSyntheticEvent<any>) => void;
9494
onRequestClose?: ?DirectEventHandler<null>,
@@ -147,6 +147,12 @@ export type ModalPropsIOS = {
147147
// | ((event: NativeSyntheticEvent<any>) => void)
148148
// | undefined;
149149
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,
150+
151+
/**
152+
* Controls whether the modal can be dismissed by swiping down on iOS.
153+
* This requires you to implement the `onRequestClose` prop to handle the dismissal.
154+
*/
155+
allowSwipeDismissal?: ?boolean,
150156
};
151157

152158
export type ModalPropsAndroid = {
@@ -192,6 +198,16 @@ function confirmProps(props: ModalProps) {
192198
'Modal with translucent navigation bar and without translucent status bar is not supported.',
193199
);
194200
}
201+
202+
if (
203+
Platform.OS === 'ios' &&
204+
props.allowSwipeDismissal === true &&
205+
!props.onRequestClose
206+
) {
207+
console.warn(
208+
'Modal requires the onRequestClose prop when used with `allowSwipeDismissal`. This is necessary to prevent state corruption.',
209+
);
210+
}
195211
}
196212
}
197213

@@ -327,6 +343,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
327343
onStartShouldSetResponder={this._shouldSetResponder}
328344
supportedOrientations={this.props.supportedOrientations}
329345
onOrientationChange={this.props.onOrientationChange}
346+
allowSwipeDismissal={this.props.allowSwipeDismissal}
330347
testID={this.props.testID}>
331348
<VirtualizedListContextResetter>
332349
<ScrollView.Context.Provider value={null}>

packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5689,6 +5689,7 @@ export type ModalPropsIOS = {
56895689
>,
56905690
onDismiss?: ?() => void,
56915691
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,
5692+
allowSwipeDismissal?: ?boolean,
56925693
};
56935694
export type ModalPropsAndroid = {
56945695
hardwareAccelerated?: ?boolean,

packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.mm

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ - (instancetype)init
2222
}
2323
_touchHandler = [RCTSurfaceTouchHandler new];
2424

25-
self.modalInPresentation = YES;
26-
2725
return self;
2826
}
2927

packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ - (RCTFabricModalHostViewController *)viewController
124124
_viewController = [RCTFabricModalHostViewController new];
125125
_viewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
126126
_viewController.delegate = self;
127+
_viewController.modalInPresentation = YES;
127128
}
128129
return _viewController;
129130
}
@@ -239,6 +240,7 @@ - (void)prepareForRecycle
239240

240241
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
241242
{
243+
const auto &oldViewProps = static_cast<const ModalHostViewProps &>(*_props);
242244
const auto &newProps = static_cast<const ModalHostViewProps &>(*props);
243245

244246
#if !TARGET_OS_TV
@@ -251,6 +253,10 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
251253

252254
self.viewController.modalPresentationStyle = presentationConfiguration(newProps);
253255

256+
if (oldViewProps.allowSwipeDismissal != newProps.allowSwipeDismissal) {
257+
self.viewController.modalInPresentation = !newProps.allowSwipeDismissal;
258+
}
259+
254260
_shouldPresent = newProps.visible;
255261
[self ensurePresentedOnlyIfNeeded];
256262

@@ -283,6 +289,16 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)co
283289
}
284290
}
285291

292+
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
293+
{
294+
auto eventEmitter = [self modalEventEmitter];
295+
const auto &props = static_cast<const ModalHostViewProps &>(*_props);
296+
297+
if (eventEmitter && props.allowSwipeDismissal) {
298+
eventEmitter->onRequestClose({});
299+
}
300+
}
301+
286302
@end
287303

288304
#ifdef __cplusplus

packages/react-native/React/Views/RCTModalHostView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
@property (nonatomic, copy) RCTDirectEventBlock onShow;
2626
@property (nonatomic, assign) BOOL visible;
27+
@property (nonatomic, assign) BOOL allowSwipeDismissal;
2728

2829
// Android only
2930
@property (nonatomic, assign) BOOL statusBarTranslucent;

packages/react-native/React/Views/RCTModalHostView.m

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
3535
if ((self = [super initWithFrame:CGRectZero])) {
3636
_bridge = bridge;
3737
_modalViewController = [RCTModalHostViewController new];
38+
_modalViewController.modalInPresentation = YES;
3839
UIView *containerView = [UIView new];
3940
containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
4041
_modalViewController.view = containerView;
@@ -50,6 +51,14 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
5051
return self;
5152
}
5253

54+
- (void)setAllowSwipeDismissal:(BOOL)allowSwipeDismissal
55+
{
56+
if (_allowSwipeDismissal != allowSwipeDismissal) {
57+
_allowSwipeDismissal = allowSwipeDismissal;
58+
_modalViewController.modalInPresentation = !allowSwipeDismissal;
59+
}
60+
}
61+
5362
- (void)notifyForBoundsChange:(CGRect)newBounds
5463
{
5564
if (_reactSubview && _isPresented) {
@@ -70,6 +79,13 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)co
7079
}
7180
}
7281

82+
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
83+
{
84+
if (_onRequestClose != nil && _allowSwipeDismissal) {
85+
_onRequestClose(nil);
86+
}
87+
}
88+
7389
- (void)notifyForOrientationChange
7490
{
7591
if (!_onOrientationChange) {

packages/react-native/React/Views/RCTModalHostViewController.m

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ - (instancetype)init
2222
return nil;
2323
}
2424

25-
self.modalInPresentation = YES;
26-
2725
_preferredStatusBarStyle = [RCTUIStatusBarManager() statusBarStyle];
2826
_preferredStatusBarHidden = [RCTUIStatusBarManager() isStatusBarHidden];
2927

packages/react-native/React/Views/RCTModalHostViewManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ - (void)invalidate
119119
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
120120
RCT_EXPORT_VIEW_PROPERTY(visible, BOOL)
121121
RCT_EXPORT_VIEW_PROPERTY(onRequestClose, RCTDirectEventBlock)
122+
RCT_EXPORT_VIEW_PROPERTY(allowSwipeDismissal, BOOL)
122123

123124
// Fabric only
124125
RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock)

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5306,6 +5306,7 @@ public class com/facebook/react/viewmanagers/ModalHostViewManagerDelegate : com/
53065306
}
53075307

53085308
public abstract interface class com/facebook/react/viewmanagers/ModalHostViewManagerInterface : com/facebook/react/uimanager/ViewManagerWithGeneratedInterface {
5309+
public abstract fun setAllowSwipeDismissal (Landroid/view/View;Z)V
53095310
public abstract fun setAnimated (Landroid/view/View;Z)V
53105311
public abstract fun setAnimationType (Landroid/view/View;Ljava/lang/String;)V
53115312
public abstract fun setHardwareAccelerated (Landroid/view/View;Z)V

0 commit comments

Comments
 (0)