禁用手势下拉表单/页面模态呈现

124
在iOS 13中,使用表单和页面样式的模态呈现可以通过向下拖动手势来解除。在我的某个表单中,这是有问题的,因为用户会在此框中绘制图案,这会干扰手势。它会将屏幕拉下来,而不是画出垂直线。
如何禁用以表单形式呈现的模态视图控制器中的垂直滑动关闭手势?
设置 isModalInPresentation = true仍然允许下拉该sheet,只是不会关闭。

2
在Apple Developer上有一份详细说明文档:https://developer.apple.com/documentation/uikit/view_controllers/disabling_pulling_down_a_sheet - Stleamist
3
但是他们没有解释如何处理当该手势干扰其他人时,就像这个问题所问的那样。 - teradyl
17个回答

142
一般情况下,不建议禁用滑动以解除功能,因为用户希望所有表单/页面表现在所有应用程序中都是相同的。相反,您可能希望考虑使用全屏幕演示样式。如果您确实想使用无法通过滑动解除的表格,请设置isModalInPresentation=true,但请注意,这仍然允许按垂直方向拉下表格,在释放触摸时它会弹回来。查看UIAdaptivePresentationControllerDelegate文档,以便在用户尝试通过滑动将其解除等操作时作出反应。
如果您的应用程序的手势或触摸处理受到滑动取消功能的影响,我从一位苹果工程师那里得到了一些修复建议。
如果您可以防止系统的平移手势识别器开始工作,这将防止姿态解除。几种方法可以做到这一点:
1. 如果您的画布绘制是由手势识别器完成的,例如您自己的UIGestureRecognizer子类,则必须在表格的解除手势之前进入began阶段。如果您能像UIPanGestureRecognizer一样快速识别,则会获胜,并且表格的解除手势将被挫败。
  • 如果您的画布绘图是使用手势识别器完成的,请使用-shouldBeRequiredToFailByGestureRecognizer:(或相关的委托方法)设置动态失败要求,其中如果传入的手势识别器是一个UIPanGestureRecognizer,则返回NO

  • 如果您的画布绘制是通过手动触摸处理完成的(例如touchesBegan:),请在您的触摸处理视图上覆盖-gestureRecognizerShouldBegin并在传入的手势识别器是UIPanGestureRecognizer时返回NO

  • 对于我的设置,方案#3非常有效。这使用户可以在绘图画布之外的任何地方向下滑动以关闭(例如导航栏),同时允许用户绘制而不移动该页,就像预期一样。

    我不能推荐尝试找到禁用手势的方法,因为它似乎相当动态,并且在切换不同大小类别时可以重新启用自己,而这在未来的版本中可能会改变。


    1
    我按照#2的方法,最终使用了委托方法gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)来允许一些识别器一起工作,而其他识别器则不行。 - Doug
    1
    我似乎无法让它们中的任何一个起作用?对于我的情况有什么建议吗?我正在展示一个屏幕,其中包含一个使用拖动选择的照片选择系统,就像iOS库存照片应用程序使用的那样。这使用了一个平移手势识别器和一个UICollectionView。如果我错了,请原谅,但是难道我们不必成为解除识别器的代表才能利用这些方法吗? - simonthumper
    6
    #3 对于我的应用程序非常有效,该应用程序允许用户在视图中绘制。 - Rory Prior
    2
    @Jordan H,你能用一些代码更好地解释第3点吗? - Giorgio
    5
    做得好!非常有趣。#3 对我来说完美无缺。 - Artem Kirillov
    显示剩余8条评论

    41

    这个手势可以在模态视图控制器的presentedView属性中找到。调试时,该属性的gestureRecognizers数组只有一个项目,并将其打印出来的结果类似于以下内容:

    UIPanGestureRecognizer: 0x7fd3b8401aa0 (_UISheetInteractionBackgroundDismissRecognizer);

    因此,要禁用此手势,您可以执行以下操作:

    let vc = UIViewController()
    
    self.present(vc, animated: true, completion: {
      vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = false
    })
    
    要重新启用它,只需将 isEnabled 设置为 true:
    vc.presentationController?.presentedView?.gestureRecognizers?[0].isEnabled = true
    
    请注意,iOS 13仍处于测试版阶段,因此在未来的版本中可能会添加更简单的方法。
    尽管目前这种解决方案似乎可行,但我不建议使用它,因为在某些情况下它可能无法正常工作或在未来的iOS发布中可能被更改,进而影响您的应用程序。

    1
    请注意,如果您在此视图控制器中有一个滚动视图,则似乎仍然可以在到达顶部后向下拉以关闭。但是,如果没有滚动视图,则这很好。 - Jordan H
    1
    @JordanH 没错!似乎滚动视图的另一个手势也以某种方式处理了模态窗口的关闭。我打印了滚动视图手势,发现有一个 UISwipeDismissalGestureRecognizer。这可能是问题所在。 - M Reza
    7
    UITableView 也会创建 _UISwipeDismissalGestureRecognizer。此外,如果您创建了一个带有根视图控制器的导航控制器,并将堆栈以页面/表单表的形式模态呈现,并在其上推入另一个视图控制器,则通过从 UIView 层次结构中的更高位置创建的手势识别器向下滑动以解除的手势将解除整个堆栈。在没有苹果显式支持禁用向下滑动以解除触摸事件处理的情况下,唯一可靠的解决方案(截至 Xcode 11 beta 3)是使用 UIModalPresentationStyleUIModalPresentationFullScreen - Gary
    5
    如果你想更安全地禁用特定手势,你甚至可以按名称或类型进行搜索。对于 guestures 中的每个手势,如果 gesture.name 等于 "_UISheetInteractionBackgroundDismissRecognizer",则将 gesture.isEnabled 设置为 false。 - spfursich
    1
    这个解决方案在iOS 16中仍然可以正常工作,很酷 :)。 - sabiland

    40

    在展示的 ViewController 的 viewDidLoad 中使用此方法:

    if #available(iOS 13.0, *) {
        self.isModalInPresentation = true
    }
    

    11
    正如问题中所指出的,“isModalInPresentation = true”仍然允许下拉表单,但它不会被关闭,这可能是您需要的,或者根据您的用例可能会出现问题,就像我的绘图画布一样。 - Jordan H
    4
    没问题,我可以为您进行翻译。最简单的解决方案是使用旧的全屏模态样式:self.modalPresentationStyle = .fullScreen。我会尽力使翻译更加通俗易懂,但不会改变原意或添加其他内容,也不会提供解释。 - Zoltan Vinkler
    1
    viewController.isModalInPresentation = true 对我有用。 - BharathRao
    1
    这是正确的答案,你仍然保留下拉动画,但你从未通过它关闭视图。谢谢! - Radu Ursache
    1
    对我来说可以用。比移除手势识别器更好。 - Benoit Deldicque

    23

    就我而言,我有一个模态屏幕,其中的视图接收触摸以捕获客户签名。

    禁用导航控制器中的手势识别器解决了问题,完全防止了模态交互式解除触发。

    我们的模态视图控制器中实现了以下方法,并通过自定义签名视图通过委托调用。

    touchesBegan 调用:

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

    touchesEnded 调用:

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

    这里有一个GIF,展示了这个行为的情况:enter image description here

    这个被标记为重复的问题更好地描述了我遇到的问题:在iOS 13上,当从主视图拖动时禁用呈现的视图控制器的交互式取消


    2
    它在iOS 14上无法使用;navigationController?.presentationController?.presentedView总是为nil。我尝试使用navigationController?.presentationController?.presentedViewController.view,但是找不到_UISheetInteractionBackgroundDismissRecognizer... - Ido
    奇怪,刚在iOS 14上测试了一下,我们的应用程序运行良好。使用Xcode 12构建,不确定是否会有所不同。 - Eneko Alonso
    我的错误,我重新打开了一些旧项目,并忘记了它是为修复另一个屏幕上的问题而设计的,在iOS 14上运行良好,感谢您的验证和回答! - Ido
    在我的情况下,navigationController 总是空的,因此我能够使用 presentationController?.presentedView?.gestureRecognizers?.forEach 访问它。 - Chuck Krutsinger
    几乎对我有用,除了要禁止的手势识别器是self.presentationController?.presentedView?.gestureRecognizers?而不是navigationController?.presentationController?.presentedView?.gestureRecognizers?。请参见@M Reza的答案。 - J Kasparian
    有什么办法可以使这个与tableview兼容吗?我尝试在scrollViewWillBeginDraggingscrollViewWillEndDragging中调用(以及使用self.presentationController而不是包括navigationController),但是当我滚动tableview时,模态仍然会有时下拉。 - thesuffering

    15

    没有必要重复造轮子。只需要在你的destinationViewController上采用UIAdaptivePresentationControllerDelegate协议,然后实现相关方法即可:

    func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
        return false
    }
    
    例如,假设您的目标视图控制器已经准备好像下面这样进行转场:

    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
    
        }
    }
    

    然后在采用上述协议的destinationVC上,你可以实现描述的方法func presentationControllerShouldDismiss(_ presentationController:) -> Bool或其他任何方法,以便正确处理自定义行为。


    这应该是被接受的答案。它以最清晰的方式正确地解决了OP的问题。我刚刚实现了它,它的工作方式完全符合您的期望。这样你也可以根据任何条件返回true或false。 - nickdnk
    2
    不必在前一个视图控制器的 prepare(for:sender:) 中设置委托,您可以在 viewDidLoad 中设置 presentationController?.delegate = self。这样所有代码都在一个视图控制器中。 - Allanah Fowler

    15

    如果全屏显示,您可以更改演示文稿的样式,此时下拉以解除禁用

    navigationCont.modalPresentationStyle = .fullScreen
    

    2
    我认为这是正确的答案。我实际上使用了isModalInPresentation来确保它完美地工作。对我来说关键是在父级中设置这些内容。当我尝试在被呈现的控制器中的viewDidLoad中设置时,它并没有起作用。 - biomiker
    我认为这也是处理这个问题的正确方式。如果是全屏,你无法滑动并解除它?我已经尝试过了,确实如此,所以这是正确的。 - Legend_ 33

    5

    你可以在viewDidAppear()方法中首先获取对UIPanGestureRecognizer进行处理的引用,以进行页面表单的关闭。请注意,在viewWillAppear()或viewDidLoad()方法中,此引用为nil。然后,您只需禁用它。

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

    如果你想进行更多的自定义,而不是完全禁用它,例如在页面表中使用navBar时,将UIPanGestureRecognizer的代理设置为你自己的视图控制器。这样,你就可以在实现时专门在你的contentView中禁用手势识别器,同时保持它在你的navBar区域活动状态。

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {}
    

    4
    您可以使用UIAdaptivePresentationControllerDelegate方法presentationControllerDidAttemptToDismiss并在presentedView上禁用gestureRecognizer。类似这样:
    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {      
        presentationController.presentedView?.gestureRecognizers?.first?.isEnabled = false
    }
    

    3

    对于每个在运行Jordans解决方法#3时遇到问题的人。

    您需要查找被呈现的根视图控制器,这取决于您的视图堆栈,这可能不是您当前的视图。

    我不得不寻找我的导航控制器PresentationViewController。

    顺便说一句@Jordam:谢谢!

    UIGestureRecognizer *gesture = [[self.navigationController.presentationController.presentedView gestureRecognizers] firstObject];
    if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
        UIPanGestureRecognizer * pan = (UIPanGestureRecognizer *)gesture;
        pan.delegate = self;
    }
    

    2

    iOS 13中

    if #available(iOS 13.0, *) {
        obj.isModalInPresentation = true
    } else {
        // Fallback on earlier versions
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接