禁用手势下拉 form/page sheet 模态呈现

Disable gesture to pull down form/page sheet modal presentation

在 iOS 13 个使用表单和页面 sheet 样式的模态演示文稿中,可以使用向下平移手势关闭。这在我的一种形式 sheet 中是有问题的,因为用户绘制到这个干扰手势的框中。它会将屏幕向下拉,而不是画一条垂直线。

如何在显示为 sheet 的模态视图控制器中禁用垂直滑动以消除手势?

设置isModalInPresentation = true仍然允许sheet被下拉,只是不会关闭。

这个手势可以在模态视图控制器的 presentedView 属性 中找到。在我调试时,这个 属性 的 gestureRecognizers 数组只有一个项目,打印它的结果是这样的:

UIPanGestureRecognizer: 0x7fd3b8401aa0 (_UISheetInteractionBackgroundDismissRecognizer);

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

let vc = UIViewController()

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

到re-enable它只需将isEnabled设置回true:

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

请注意,iOS13 仍处于测试阶段,因此可能会在即将发布的版本中添加更简单的方法。

尽管此解决方案目前似乎有效,但我不推荐它,因为它在某些情况下可能无效,或者可能会在未来 iOS 版本中更改并可能影响您的应用程序。

对于导航控制器,为了避免呈现视图的滑动交互,我们可以使用:

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

一般来说,您不应尝试禁用滑动关闭功能,因为用户希望所有 form/page sheet 在所有应用程序中的行为都相同。相反,您可能需要考虑使用全屏演示样式。如果您确实想使用无法通过滑动关闭的 sheet,请设置 isModalInPresentation = true,但请注意,这仍然允许 sheet 被垂直拉下并且它会弹回释放触摸后。查看 UIAdaptivePresentationControllerDelegate 文档以在用户尝试通过滑动等操作关闭它时做出反应。

如果您的应用程序的手势或触摸处理受到滑动关闭功能的影响,我确实收到了 Apple 工程师关于如何解决该问题的一些建议。

如果您可以阻止系统的平移手势识别器开始,这将防止手势关闭。几种方法:

  1. 如果您的 canvas 绘图是使用手势识别器完成的,例如您自己的 UIGestureRecognizer 子类,请在 [=53= 之前进入 began 阶段] 的解雇手势。如果你认得快到UIPanGestureRecognizer,你就赢了,sheet的解散手势将被颠覆。

  2. 如果您的 canvas 绘图是使用手势识别器完成的,请使用 -shouldBeRequiredToFailByGestureRecognizer:(或相关的委托方法)设置动态失败要求,其中您 return NO 如果传入的手势识别器是 UIPanGestureRecognizer.

  3. 如果您的 canvas 绘图是通过手动触摸处理完成的(例如 touchesBegan:),请覆盖触摸处理视图上的 -gestureRecognizerShouldBegin,并且 return NO 如果传入的手势识别器是 UIPanGestureRecognizer.

事实证明,我的设置 #3 运行良好。这允许用户在绘图 canvas 之外的任何地方向下滑动以关闭(如导航栏),同时允许用户在不移动 sheet 的情况下进行绘图,正如人们所期望的那样。

我不建议尝试找到禁用它的手势,因为它看起来相当动态,并且可以在不同尺寸之间切换时重新启用它自己 类 例如,这可能会在未来的版本中改变。

在呈现的 ViewController viewDidLoad:

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

您可以更改演示文稿样式,如果它在全屏模式下,关闭下拉菜单将被禁用

navigationCont.modalPresentationStyle = .fullScreen

您可以使用 UIAdaptivePresentationControllerDelegate 方法 presentationControllerDidAttemptToDismiss 并禁用 presentedView 上的 gestureRecognizer。 像这样:

func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {      
    presentationController.presentedView?.gestureRecognizers?.first?.isEnabled = false
}

我,我用这个:

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

对于乔丹解决方案#3 有问题的每个人运行。

您必须寻找正在显示的 ROOT viewcontroller,这取决于您的视图堆栈,这可能不是您当前的视图。

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

顺便说一句@Jordam:谢谢!

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

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

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

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

调用自 touchesBegan:

private func disableDismissalRecognizers() {
    navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
        [=10=].isEnabled = false
    }
}

调用自 touchesEnded:

private func enableDismissalRecognizers() {
    navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
        [=11=].isEnabled = true
    }
}

这是一个显示行为的 GIF:

这个被标记为重复的问题更好地描述了我遇到的问题:Disabling interactive dismissal of presented view controller on iOS 13 when dragging from the main view

将尝试更详细地描述@Jordan H 已经建议的方法 2:

1) 为了能够捕获模态 sheet 的平移手势并做出决定,将其添加到视图控制器的 viewDidLoad:

navigationController?.presentationController?.presentedView?.gestureRecognizers?.forEach {
   [=10=].delegate = self
}

2) 使用 gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)

启用捕捉平移手势和您自己的手势的能力

3) 实际决定可以在gestureRecognizer(_:shouldBeRequiredToFailBy:)

示例代码,使滑动手势优先于 sheet 的平移手势(如果两者都存在)。它不会影响没有滑动手势识别器的区域中的原始平移手势,因此原始 "swipe to dismiss" 仍然可以按设计工作。

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

在我的例子中,我只有几个滑动手势识别器,所以比较类型对我来说就足够了,但如果有更多类型,比较手势识别器本身可能有意义(以编程方式添加的或作为接口的出口构建器),如本文档中所述:https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/coordinating_multiple_gesture_recognizers/preferring_one_gesture_over_another

以下是代码在我的案例中的工作方式。没有它,滑动手势大多被忽略,只是偶尔起作用。

在IOS 13

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

UITableViewUICollectionView 启动页面的情况下 sheet 当用户试图滚动超过滚动视图的顶端时关闭手势,这个手势可以是通过添加一个不可见的 UIRefreshControl 立即调用 endRefreshing 来禁用。

另见

准备中(for:sender:) :

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

或者,在您初始化控制器之后:

let controller = YourViewController()
controller.modalPresentationStyle = .fullScreen

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

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

如果您想要更多自定义而不是完全禁用它,例如,在页面中使用导航栏时 sheet,请将该 UIPanGestureRecognizer 的委托设置为您自己的视图控制器。这样,您可以在您的 contentView 中专门禁用手势识别器,同时通过实现

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

无需重新发明轮子。很简单,在你的destinationViewController上采用UIAdaptivePresentationControllerDelegate协议,然后实现相关的方法:

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

例如,假设您的 destinationViewController 已准备好进行如下所示的 segue:

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或任何其他方法,以便正确处理您的自定义行为。