Disable gesture to pull down form/page sheet modal presentation
Asked Answered
I

17

125

In iOS 13 modal presentations using the form and page sheet style can be dismissed with a pan down gesture. This is problematic in one of my form sheets because the user draws into this box which interferes with the gesture. It pulls the screen down instead of drawing a vertical line.

How can you disable the vertical swipe to dismiss gesture in a modal view controller presented as a sheet?

Setting isModalInPresentation = true still allows the sheet to be pulled down, it just won't dismiss.

Irmine answered 22/6, 2019 at 19:36 Comment(2)
There is a well-explained document on the Apple Developer: developer.apple.com/documentation/uikit/view_controllers/…Tamas
But they don't explain how to solve when that gesture interferes with others as this question is asking.Maryrosemarys
I
143

In general, you shouldn't try to disable the swipe to dismiss functionality, as users expect all form/page sheets to behave the same across all apps. Instead, you may want to consider using a full-screen presentation style. If you do want to use a sheet that can't be dismissed via swipe, set isModalInPresentation = true, but note this still allows the sheet to be pulled down vertically and it'll bounce back up upon releasing the touch. Check out the UIAdaptivePresentationControllerDelegate documentation to react when the user tries to dismiss it via swipe, among other actions.

If you have a scenario where your app's gesture or touch handling is impacted by the swipe to dismiss feature, I did receive some advice from an Apple engineer on how to fix that.

If you can prevent the system's pan gesture recognizer from beginning, this will prevent the gestural dismissal. A few ways to do this:

  1. If your canvas drawing is done with a gesture recognizer, such as your own UIGestureRecognizer subclass, enter the began phase before the sheet’s dismiss gesture does. If you recognize as quickly as UIPanGestureRecognizer, you will win, and the sheet’s dismiss gesture will be subverted.

  2. If your canvas drawing is done with a gesture recognizer, setup a dynamic failure requirement with -shouldBeRequiredToFailByGestureRecognizer: (or the related delegate method), where you return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.

  3. If your canvas drawing is done with manual touch handling (e.g. touchesBegan:), override -gestureRecognizerShouldBegin on your touch handling view, and return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.

With my setup #3 proved to work very well. This allows the user to swipe down anywhere outside of the drawing canvas to dismiss (like the nav bar), while allowing the user to draw without moving the sheet, just as one would expect.

I cannot recommend trying to find the gesture to disable it, as it seems to be rather dynamic and can reenable itself when switching between different size classes for example, and this could change in future releases.

Irmine answered 24/8, 2019 at 4:44 Comment(13)
I followed #2 but ended up using the delegate method gestureRecognizer(_:,shouldRecognizeSimultaneouslyWith:) to allow some recognizers to work together and others to not.Florance
I can't seem to get any of these to work? Any advice for my scenario? I'm presenting a screen with a photo-selection system using the drag to select that the stock iOS photo app uses. This uses a pan gesture recogniser and a UICollectionView. Forgive me if I'm wrong, but wouldn't we have to become the dismiss recogniser's delegate to make use of any of these methods?Priming
Never mind! Was using shouldRequireFailureOf instead of shouldBeRequiredToFailBy 🤦‍♂️Priming
Coming from Android I was searching for a way my custom Views could take over touch and gestureRecognizerShouldBegin is perfect! Thank you!Clementeclementi
#3 worked great for my app that allows the user to draw in a view.Turning
@Jordan H Could you explain better #3 with some code?Orsa
@jordan-h I'm also having trouble with getting #3 working. I have added different recognizers (tap, pan, long press..) to both the view controller's view and the drawing view, with no luck. The modal still moves when drawing on the view.Smelt
Well done! Very interesting. #3 works perfect for me.Assiduity
All of this extra work because Apple felt like changing the modal presentation style...Maxia
For #2, I needed to distinguish the UIPanGestureRecognizer I owned from one controlling a UITableView/UIScrollView and the other controlling the modal. All three of them are UIPanGestureRecognizers, so checking their type doesn't help. Next best thing to do is check the view that they're targeting. The modal gesture recognizer targets a UIDropShadowView, which is a private API, so you can't check for that explicitly. Instead, depending on how your view hierarchy is laid out, you can check to see if that UIDropShadowView isn't a subview of your own in order to rule it out.Simonson
#3 works very well for me with a signature capture formAllman
#3 also worked for a UIControl that implemented the "tracking" methods to handle touches - I used this inside the func: ` return isTracking ? !(gestureRecognizer is UIPanGestureRecognizer) : true`Deeannadeeanne
as already mentioned in some other comments, the third approach won't work if your view hierarchy contains other UIPanGestureRecognizers (eg from a UIScrollView). for me making the check a bit more explicit (return !(gestureRecognizer is UIPanGestureRecognizer && gestureRecognizer.name == "_UISheetInteractionBackgroundDismissRecognizer")) did the trick. the name can change at any time so beware (though it seems to have been stable since at least iOS 13)Bourges
W
41

This gesture can be found in the modal view controller's presentedView property. As I debugged, the gestureRecognizers array of this property has only one item and printing it resulted in something like this:

UIPanGestureRecognizer: 0x7fd3b8401aa0 (_UISheetInteractionBackgroundDismissRecognizer);

So to disable this gesture you can do like below:

let vc = UIViewController()

self.present(vc, animated: true, completion: {
  vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = false
})

To re-enable it simply set isEnabled back to true:

vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = true

Note that iOS 13 is still in beta so a simpler approach might be added in an upcoming release.

Although this solution seems to work at the moment, I would not recommend it as it might not work in some situations or might be changed in future iOS releases and possibly affect your app.

Waldron answered 23/6, 2019 at 5:7 Comment(5)
Note that if you have a scroll view in this view controller it appears you can still pull down to dismiss once you've reached the top. But if you don't have a scroll view this is great.Irmine
@JordanH That's right! It seems another gesture from the scroll view is also handling the modal dismissal somehow. I printed the scroll view gestures and there's a UISwipeDismissalGestureRecognizer. This could be the issue.Waldron
UITableView creates _UISwipeDismissalGestureRecognizer as well. Moreover, if you create a navigation controller with a root view controller, present the stack modally as a page/form sheet and push another view controller on top of it, the swipe-down-to-dismiss gesture will dismiss the entire stack through a gesture recognizer created somewhere higher up the UIView hierarchy. Without Apple's explicit support for disabling swipe-down-to-dismiss touch event processing, the only reliable solution (as of Xcode 11 beta 3) is to use UIModalPresentationStyle of UIModalPresentationFullScreen.Csch
You can even search by name or type if you want to be safer about which gesture you are disabling. for gesture in guestures where gesture.name == "_UISheetInteractionBackgroundDismissRecognizer" { gesture.isEnabled = false }Amphicoelous
This solution still works ok in iOS 16, cool :).Edlyn
A
40

Use this in the presented ViewController viewDidLoad:

if #available(iOS 13.0, *) {
    self.isModalInPresentation = true
}
Analcite answered 26/9, 2019 at 9:25 Comment(5)
As noted in the question, isModalInPresentation = true still allows the sheet to be pulled down, it just won't dismiss, which may be exactly what you need or it can be problematic depending on your use case like it was for my drawing canvas.Irmine
You are right. I think the most simple solution is use old fullscreen modal style: self.modalPresentationStyle = .fullScreenAnalcite
viewController.isModalInPresentation = true worked for meAshleyashli
this is the right answer, you still keep the animation of pulling down but you never close the view by that. thanks!Tapp
Works for me. Better than removing gesture recognisers.Reviviscence
S
23

In my case, I have a modal screen with a view that receives touches to capture customer signatures.

Disabling the gesture recognizer in the navigation controller solved the problem, preventing the modal interactive dismissal from being triggered at all.

The following methods are implemented in our modal view controller, and are called via delegate from our custom signature view.

Called from touchesBegan:

private func disableDismissalRecognizers() {
    navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
        $0.isEnabled = false
    }
}

Called from touchesEnded:

private func enableDismissalRecognizers() {
    navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
        $0.isEnabled = true
    }
}

Here is a GIF showing the behavior: enter image description here

This question, flagged as duplicate, describes better the issue I had: Disabling interactive dismissal of presented view controller on iOS 13 when dragging from the main view

Smelt answered 24/12, 2019 at 0:2 Comment(6)
It doesn't works on iOS 14; navigationController?.presentationController?.presentedView is always nil. I've tried using navigationController?.presentationController?.presentedViewController.view instead, but can't find _UISheetInteractionBackgroundDismissRecognizer in there...Asaasabi
Weird, just tested on iOS 14, our app works great. Built with Xcode 12, not sure if that would make a differenceSmelt
My mistake, I've reopened some old project and forgot that it meant to fix something in another screen, it's working fine on iOS 14, thank you for validate that and thank you for your answer!Asaasabi
In my case, navigationController is always nil, so I was able to access this with presentationController?.presentedView?.gestureRecognizers?.forEachDeliciadelicious
Almost worked for me, except that the gesturesRecognizers to be inhibited were self.presentationController?.presentedView?.gestureRecognizers?rather than navigationController?.presentationController?.presentedView?.gestureRecognizers?. See @M Reza 's answerMarasco
Any idea how to make this compatible with a tableview? I tried calling in scrollViewWillBeginDragging and scrollViewWillEndDragging (as well as using self.presentationController instead of including the navigationController) but the modal will still pull down sometimes when I scroll the tableview.Micrococcus
S
16

No need to reinvent the wheel. It is as simple as adopting the UIAdaptivePresentationControllerDelegate protocol on your destinationViewController and then implement the relevant method:

func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
    return false
}

For example, let's suppose that your destinationViewController is prepared for segue like below:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "yourIdentifier",
       let destinationVC = segue.destination as? DetailViewController
    {
        //do other stuff

        destinationVC.presentationController?.delegate = destinationVC

    }
}

Then on the destinationVC (that should adopt the protocol described above), you can implement the described method func presentationControllerShouldDismiss(_ presentationController:) -> Bool or any of the other ones, in order to handle correctly your custom behaviour.

Straitlaced answered 5/2, 2021 at 8:30 Comment(2)
This should be the accepted answer. It correctly addresses OP's problem in the cleanest way possible. I just implemented this and it works exactly as you would expect. This way you can also return true or false based on whatever.Eventempered
Instead of setting the delegate in prepare(for:sender:) of the previous view controller, you can have presentationController?.delegate = self in viewDidLoad. That way all of the code is in one view controller.Marguerita
A
15

you can change the presentation style, if its in full screen the pull down to dismiss would be disabled

navigationCont.modalPresentationStyle = .fullScreen
Aarhus answered 3/10, 2019 at 16:38 Comment(2)
I think this is the right answer. I actually used this together with isModalInPresentation just to be sure, and it works perfectly. The key for me was to set these in the parent. When I tried to set in viewDidLoad in the presented controller, it did NOT work.Gobble
for me i think this is also the correct way to handle this. If its full screen you cant swipe and dismiss it? have tried it and you cant so this is correctWraf
V
5

You may first get a reference to the UIPanGestureRecognizer handling the page sheet dismissal in viewDidAppear() method. Notice that this reference is nil in viewWillAppear() or viewDidLoad(). Then you simply disable it.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    presentationController?.presentedView?.gestureRecognizers?.first.isEnabled = false
}

If you want more customization rather than disabling it completely, for example, when using a navBar within the page sheet, set the delegate of that UIPanGestureRecognizer to your own view controller. That way, you can disable the gesture recognizer exclusively in your contentView while keeping it active in your navBar region by implementing

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {}
Verenaverene answered 16/10, 2020 at 22:25 Comment(0)
V
4

You can use the UIAdaptivePresentationControllerDelegate method presentationControllerDidAttemptToDismiss and disable the gestureRecognizer on the presentedView. Something like this:

func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {      
    presentationController.presentedView?.gestureRecognizers?.first?.isEnabled = false
}
Venation answered 9/10, 2019 at 21:14 Comment(0)
S
3

For every body having problems with Jordans solution #3 running.

You have to look for the ROOT viewcontroller which is beeing presented, depending on your viewstack, this is maybe not you current view.

I had to look for my navigation controllers PresentationViewController.

BTW @Jordam: Thanks!

UIGestureRecognizer *gesture = [[self.navigationController.presentationController.presentedView gestureRecognizers] firstObject];
if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
    UIPanGestureRecognizer * pan = (UIPanGestureRecognizer *)gesture;
    pan.delegate = self;
}
Shove answered 6/11, 2019 at 12:59 Comment(0)
H
2

in IOS 13

if #available(iOS 13.0, *) {
    obj.isModalInPresentation = true
} else {
    // Fallback on earlier versions
}
Hangover answered 11/4, 2020 at 12:42 Comment(0)
P
1

Me, I use this :

-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];

for(UIGestureRecognizer *gr in self.presentationController.presentedView.gestureRecognizers) {
    if (@available(iOS 11.0, *)) {
        if([gr.name isEqualToString:@"_UISheetInteractionBackgroundDismissRecognizer"]) {
            gr.enabled = false;
        }
    }
}
Posehn answered 18/10, 2019 at 15:26 Comment(0)
E
0

Will try to describe method 2 already suggested by @Jordan H in more details:

1) To be able to catch and make decisions about the modal sheet's pan gesture add this into view controller's viewDidLoad:

navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
   $0.delegate = self
}

2) Enable the ability to catch the pan gesture together with your own gestures using gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)

3) The actual decision can go in gestureRecognizer(_:shouldBeRequiredToFailBy:)

Example code, which makes the swipe gesture to be preferred over sheet's pan gesture, if both present. It doesn't affect original pan gesture in areas where there is no swipe gesture recognizer and therefore the original "swipe to dismiss" can still work as designed.

extension PeopleViewController: UIGestureRecognizerDelegate {

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if gestureRecognizer === UIPanGestureRecognizer.self && otherGestureRecognizer === UISwipeGestureRecognizer.self {
            return true
        }
        return false
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

In my case I have only a few swipe gesture recognizers, so comparing types is enough for me, but if there more of them it might make sense to compare the gestureRecognizers themselves (either programmatically added ones or as outlets from interface builder) as described in this doc: https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers/preferring_one_gesture_over_another

Here's how the code works in my case. Without it the swipe gesture was mostly ignored and worked only occasionally.

enter image description here

Escapement answered 11/2, 2020 at 14:17 Comment(2)
Hey, I've tried your solution, but I still have an issue. When I set delegate in viewDidAppear(cause my presentationController is nil, I just present a vc morally). And, I iterate its view's supervise to find out the view has an PanGesture, and set its delegate to self. Then my vc cannot swipe down to dismiss, is there any other way to solve my issue? Please helpPossible
@Weslie, its the presented view's recognizers you need to catch, not its superview's recognizers. Try to temporarily add gestureRecognizerShouldBegin delegate method to see which gestures are actually being catched and debug.Escapement
U
0

In the case when a UITableView or UICollectionView initiates the page sheet dismiss gesture when the user attempts to scroll past the top end of the scrolling view, this gesture can be disabled by adding an invisible UIRefreshControl that calls endRefreshing immediately.

See also https://mcmap.net/q/181974/-ios13-prevent-pulling-down-on-tableview-which-is-scrolled-to-top-from-dismissing-sheet-style-modally-presented-viewcontroller-duplicate

Umbilical answered 19/4, 2020 at 23:19 Comment(0)
O
0

SwiftUI since iOS 15

    .interactiveDismissDisabled()

For Example:

    .sheet(isPresented: $add) {
        AddView()
            .interactiveDismissDisabled()
    }
Obannon answered 22/7, 2022 at 14:5 Comment(0)
U
-1

iOS disable gestureRecognizers for Modal ViewController

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    //if you use UINavigationController
    self.navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
        $0.isEnabled = false
    }

    //if not
    self.presentationController?.presentedView?.gestureRecognizers?.forEach {
        $0.isEnabled = false
    }
}

presentedView returns not viewController.view object

//po navigationController?.presentationController?.presentedView
<UIDropShadowView: 0x10bd69100; frame = (0 57; 390 787); gestureRecognizers = <NSArray: 0x2821d26d0>; layer = <CALayer: 0x282f0c7c0>>

//po self.view
<UIView: 0x10bd67c90; frame = (0 0; 390 787); autoresize = W+H; backgroundColor = <UIDynamicSystemColor: 0x283ada180; name = systemBackgroundColor>; layer = <CALayer: 0x282f0ca40>>
Unbending answered 8/9, 2023 at 12:56 Comment(0)
W
-3

For navigation Controller, to avoid swipe interaction for presented view we can use:

if #available(iOS 13.0, *) {navController.isModalInPresentation = true}
Walton answered 13/8, 2019 at 11:2 Comment(1)
No, but that doesn't prevent the gesture, which is what the question is about.Tana
D
-3

In prepare(for:sender:) :

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == viewControllerSegueID {
        let controller = segue.destination as! YourViewController
        controller.modalPresentationStyle = .fullScreen
    }
}

or, after you initialize your controller:

let controller = YourViewController()
controller.modalPresentationStyle = .fullScreen
Does answered 2/10, 2020 at 10:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.