为了澄清,以下是@switz提供的情况:
- 响应
-touchesBegan:withEvent:
后,通过segue以模态方式呈现视图控制器。
- 当用户松开手指时,应该关闭视图控制器。
问题在于如何对抬起手指作出反应,因为不会调用
-touchesEnded:withEvent:
。
简短的答案是,呈现的视图控制器需要使用“全屏幕”
modalPresentationStyle
而不是默认的“全屏幕”样式(这可以作为segue的呈现样式指定,或者如果是“Default”,则作为呈现的视图控制器的呈现样式)。
长答案需要简要概述触摸处理的工作原理。此解释忽略手势识别器:
当触摸开始时,它被传递到包含触摸点的“最上层”视图。从那里,它沿着响应者链传递,直到某个对象决定处理触摸(这是通过实现
-touchesBegan:withEvent:
并不调用
super
来表示的)。
随后对触摸的更改(例如移动、结束、取消)将返回相同的接受触摸的视图。视图将继续接收触摸事件,直到触摸结束或取消。
当应用程序进入后台(例如因为电话来了)或者
UIScrollView
之类的UIKit类决定需要接管触摸处理时(因为手指移动得足够远,看起来像是用户想要滚动),触摸被取消。这里还有一些与
UIScrollView.delaysContentTouches
相关的有趣操作,但可以忽略它们。
但是有一个细节,即没有记录下来:只要视图仍然与窗口关联,就会发生触摸传递。如果被认为是“最上层”的视图(与
UITouch
相关联的视图)从窗口中移除,则认为触摸已消失,并且重要的是,不会再向任何人传递该触摸的事件。即使涉及的视图不是处理触摸的对象,这也是正确的。
而这个最终的细节就是这个问题的原因。因为默认的“全屏幕”呈现样式实际上从窗口中
删除了旧的视图控制器的视图,所以触摸处理立即停止。但是,“Over Full Screen”呈现样式不会删除它,它只是用一个视图覆盖旧的视图。“Over Full Screen”通常用于呈现的视图控制器不是完全不透明的情况,但在这种情况下,我们使用它来避免中断触摸处理。
但这还不是全部。还有另一个问题,当被触摸的视图位于一个
UIScrollView
内部时(无论它是可滚动的还是总是弹跳),即使启用了 "Over Full Screen",你会发现,在移动手指时,稍微动一下手指就会导致触摸被取消。这是因为
UIScrollView
并不知道它已经被覆盖,并且认为用户实际上是想滚动。这会导致它取消触摸。
不过,这个问题也有解决方案。虽然有点丑陋,但解决方案是在执行segue时立即取消任何包含滚动视图上的滚动。可以使用以下代码来完成这个操作:
class ViewController: UIViewController {
func touchDown(sender: UIView) {
var view = sender.superview
while view != nil {
if let scrollView = view as? UIScrollView {
let enabled = scrollView.panGestureRecognizer.enabled
scrollView.panGestureRecognizer.enabled = true
scrollView.panGestureRecognizer.enabled = enabled
}
view = view?.superview
}
performSegueWithIdentifier(identifier, sender: self)
}
}
当涉及到触摸处理时,没有讨论手势识别器是不完整的。手势识别器几乎可以改变有关触摸处理的所有内容。它们优先处理任何触摸,并且可以随时中断视图触摸处理。例如,UIScrollView的UIPanGestureRecognizer用于滚动,当它进入“began”状态(因为用户移动了足够的手指),这就会导致触摸被取消。因此,在这种情况下,最好的解决方案是根本不实现-touchesBegan:withEvent:,而是使用手势识别器。在这里最简单的解决方案是使用UILongPressGestureRecognizer,并将minimumPressDuration设置为0,allowableMovement设置为某个荒谬的高值(因为您不希望移动取消触摸)。我推荐这样做是因为UILongPressGestureRecognizer是一个连续的识别器,这意味着它将发送Began、Moved和Ended事件,并且使用建议的设置,它将在响应触摸开始、移动和结束时发送它们。更重要的是,一旦您的识别器开始处理触摸,这将自动防止任何其他识别器(如滚动视图的平移识别器)“接管”并取消触摸。
请注意,如果您将手势识别器附加到scrollView本身(例如UITableView),但只想响应特定位置的触摸(例如在行上),则需要限制识别器。您可以使用委托方法gestureRecognizer(_:shouldReceiveTouch:)来实现这一点,类似于:
func gestureRecognizer(recognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
let touchLocation = touch.locationInView(self.tableView)
if let indexPath = self.tableView.indexPathForRowAtPoint(touchLocation) {
if indexPath.section == 0 {
return true
}
}
return false
}
这样,识别器就不会在用户在表格其他地方触摸时阻止tableView的panGestureRecognizer滚动。