let tap1 = UITapGestureRecognizertarget: self, action: #selector(gesTap1))
view.addGestureRecognizer(tap1)
let tap2 = UITapGestureRecognizer(target: self, action: #selector(gesTap2))
view.addGestureRecognizer(tap2)
手势的禁止与允许
我们先看一下 Apple 官方的描述。
When a touch begins, if you can immediately determine whether or not your gesture recognizer should consider that touch, use thegestureRecognizer:shouldReceiveTouch: method. This method is called every time there is a new touch. Returning NO prevents the gesture recognizer from being notified that a touch occurred. The default value is YES. This method does not alter the state of the gesture recognizer.
If you need to wait as long as possible before deciding whether or not a gesture recognizer should analyze a touch, use thegestureRecognizerShouldBegin: delegate method. Generally, you use this method if you have a UIView or UIControl subclass with custom touch-event handling that competes with a gesture recognizer. Returning NO causes the gesture recognizer to immediately fail, which allows the other touch handling to proceed. This method is called when a gesture recognizer attempts to transition out of the Possible state, if the gesture recognition would prevent a view or control from receiving a touch.
You can use the gestureRecognizerShouldBegin:UIView method if your view or view controller cannot be the gesture recognizer’s delegate. The method signature and implementation is the same.
optional func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool
optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool
上述两个方法都是用来决定是否允许 UIGestureRecognizer
响应触摸事件的,区别在于当触摸事件发生时,
使用第一个方法可以立即控制 UIGestureRecognizer
是否对其处理,且不会修改 UIGestureRecognizer
的状态机;(时机在 手势touchesBegan
前)
使用二个方法会等待一段时间,在 UIGestureRecognizer
识别手势转换状态时调用,返回 NO
会改变其状态机,使其 state 变为 failed
。(时机在 手势touchesEnded
后)
UIView 自身也有一个 gestureRecognizerShouldBegin
方法, 当 View 不是 UIGestureRecognizer
的 delegate
时,我们可以使用这个方法来使 UIGestureRecognizer 失效。对于所有绑定到父 View 上的 UIGestureRecognizer
,除了它们本身的 delegate 之外,第一响应者也会收到这个方法的调用。
当 View 继承了gestureRecognizerShouldBegin
方法并在此处打上断点,得到的方法调用如下图所示。
上图中我们还可以看到两个没有提到过的名词,一个是UITouchesEvent
,另一个是UIGestureEnvironment
。
UITouchesEvent
通过上文列举的UIEvent
属性,我们发现其所有的属性都是只读以防止被修改,在事件响应的流程中,实际上传递的对象是UIEvent
的子类UITouchesEvent
。
UIGestureEnvironment
我们可以认为UIGestureEnvironment
是管理所有手势的上下文环境,当调用 addGestureRecognizer
方法时会将 UIGestureRecognizer
加入到其中,UIWindow 通过 sendEvent
发送事件之后,UIGestureEnvironment
接收该事件并对相关的手势进行调用,起到对手势统一管理的作用。
1. UIGestureRecognizer
首先收到触摸事件,Hit-Testing
返回的 View 延迟收到;
2. 第一个 UIGestureRecognizer
识别成功后,UIGestureEnvironment
会发起响应链的 cancel
;
3. 可以通过设置 UIGestureRecognizer
的 Properties
来控制对响应链的影响。
UIControl
事件通知方式
UIControl
作为UIResponder
的派生类,其也具有UIResponder
的touch
系列四个方法,但其内部对这四个方法进行了重写,在 touchBegin
、touchesMoved
、touchesEnded
、touchesCancelled
中实际上分别调用了以下对应的四个方法。比如 beginTracking
是在 touchesBegan
方法内部调用的。
通过下述方法参数,我们可以注意到:UIControl 处理的不是 touch 数组而是单个 touch。 也就是说:UIControl 只能处理单点触控事件。
func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool
func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool
func endTracking(_ touch: UITouch?, with event: UIEvent?)
func cancelTracking(with event: UIEvent?)
UIControl
在重写touch
系列四个方法时,其方法内部不会调用父类的方法,也就意味着UIControl
对事件响应进行了阻断,使事件不会流向nextResponder
。
关于UIControl
事件处理的流程如下:
通过 func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event)
添加事件处理的target
和action
;
当UIControl
监听到需要处理的交互事件时,会调用 func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?)
将target
、action
以及event
对象发送给全局应用。
Application
对象再通过 func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool
向target
发送action
。
可以注意到addTarget
时,target
类型是一个可选值,如传入 nil 时,Application
会自动在响应链上从上往下寻找能响应action
的对象。
UIControl
也是UIResponder
的派生类,当其父 View 添加了手势事件,自身也添加了事件响应,按照上文描述来看,其结果应该是手势事件触发,自身的事件响应不会被触发。但是根据我们的开发经验可以知道,实际的结果是手势事件不触发,自身的事件响应正常触发。那其中的原理是什么呢?它与普通的UIResponder
有何不同呢?我们先看一下 Apple 官方的一些介绍。
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer. This applies only to gesture recognition that overlaps the default action for a control, which includes:
A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.
If you have a custom subclass of one of these controls and you want to change the default action, attach a gesture recognizer directly to the control instead of to the parent view. Then, the gesture recognizer receives the touch event first. As always, be sure to read the iOS Human Interface Guidelines to ensure that your app offers an intuitive user experience, especially when overriding the default behavior of a standard control.
通过上边的描述我们可以得出原因,对于系统UIControl
(除去开发者自定义的)来说,为了防止 UIControl
默认的手势与其父 View 上的 UIGestureRecognizer
的冲突,系统会默认设定,UIControl
来响应触摸事件。
原因我们找到了,下面来介绍一下里面涉及到的原理。
上节UIGestureRecognizer
中介绍过gestureRecognizerShouldBegin
方法对手势有决定是否响应的作用,UIControl
便是利用这一点达到了上述效果。
UIControl
内部重写了 UIView 提供的的gestureRecognizerShouldBegin
方法,返回 false
,使父 View 上的手势不参与到事件响应中去,但是不会影响其自身的手势。
1. UIButton 会截断响应链的事件传递,也可以利用响应链来寻找 Action Method。
2. UIGestureRecognizer 仍然会先于 UIControl 接收到触摸事件;
3. UIButton 等系统 UIControl 会拦截其父 View 上的 UIGestureRecognizer,但不会拦截自己和子 View 上的 UIGestureRecognizer;
这里再介绍一下UIScrollView
处理触摸事件的特殊之处及其原理。
当用户在 UIScrollView
的一个子视图上按下时,UIScrollView
并不知道用户是想要滑动内容视图还是点击对应子视图,所以在按下的一瞬间, 事件 UIEvent
从 UIApplication
传递到 UIScrollView
后,其会先将该事件拦截而不会立即传递给对应的子视图, 同时开始一个 150ms 的倒计时,并监听用户接下来的行为。
当倒计时结束前,如果用户的手指发生了移动,直接滚动内容视图,不会将该事件传递给对应的子视图;
当倒计时结束时,如果用户的手指位置没有改变,则调用自身的 -touchesShouldBegin:withEvent:inContentView:
方法询问是否将事件传递给对应的子视图 (如果返回 NO, 则该事件不会传递给对应的子视图,如果返回 YES,则该事件会传递给对应的子视图,默认为 YES);
当事件被传递给子视图后, 如果手指位置又发生了移动, 则调用自身的 -touchesShouldCancelInContentView:
方法询问是否取消已经传递给子视图的事件。
open var delaysContentTouches: Bool
open var canCancelContentTouches: Bool
open func touchesShouldBegin(_ touches: Set<UITouch>, with event: UIEvent?, in view: UIView) -> Bool
open func touchesShouldCancel(in view: UIView) -> Bool
通过阅读本文,我想你对下面的问题出现的原因及解决办法应该有了比较深刻的认识。
UICollectionView 父 view 添加手势,其内部代理 didSelectItemAt
不触发
tapViewGesture.delegate = self
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let p = gestureRecognizer.location(in: superview)
let v = superview.hitTest(p, with: nil)
return v == gestureRecognizer.view
最后,附上戴铭老师本周博文《我写技术文章的一点心得》中的一段话,我觉得很有共鸣。
写文章并不是最终的目的,写作是你对自己思想的研究和开发。文章的上限是你的技术能力,文章只是让人了解你技术一种手段。因此更重要的是你做的技术是否有突破有演进,获得应用,并在产品中取得了好的效果。还有那些孤独着研究技术的时光,经历着一直努力着奋斗着却一直不被看见,得不到认同,也没有结果的岁月,还能够一直被自己的热情感动而不放弃去取得一点点进步带来的满足感
新的一周要更加努力呀!
Let's be CoderStar!
由手势与 UIControl 冲突引发的「事件处理全家桶」探索
iOS 事件(UITouch、UIControl、UIGestureRecognizer)传递机制
iOS | 事件传递及响应链
iOS 触摸事件全家桶
有一个技术的圈子与一群同道众人非常重要,来我的技术公众号,这里只聊技术干货。