0%

响应者链

响应者对象

响应者对象(Responder Object)指的是有响应和处理事件能力的对象。A responder object is any instance of the UIResponder class, and common subclasses include UIView, UIViewController, and UIApplication. UIResponder是所有响应者对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的AppDelegate、UIApplication、 UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。

响应者链

响应者链就是由一系列的响应者对象构成的一个层次结构。UIResponder对象能够接收触摸事件(其子类当然也能够)。每一个UIResponder对象都有一个指针nextResponder,然后这些链接在一起的对象就组成了响应者链。

系统是如何找到第一响应者的

当用户点击了某个视图.系统是如何找到用户点击的视图呢?

答案就是对视图进行hitTest.

官方文档:

UIKit uses view-based hit testing to determine where touch events occur. Specifically, UIKit compares the touch location to the bounds of view objects in the view hierarchy. The hitTest:withEvent: method of UIView walks the view hierarchy, looking for the deepest subview that contains the specified touch. That view becomes the first responder for the touch event.

注意:If a touch location is outside of a view’s bounds, the hitTest:withEvent: method ignores that view and all of its subviews. As a result, when a view’s clipsToBounds property is NO, subviews outside of that view’s bounds are not returned even if they happen to contain the touch.

相关API:

1
2
3
open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

open func point(inside point: CGPoint, with event: UIEvent?) -> Bool // default returns YES if point is in bounds

举个例子:界面如下:

下面的视图为XQ1View,上面有三个子视图分别为XQ10View,XQ11View,XQ12View。

当用户点击了XQ11View,系统是如何找到它的?

具体来说就是:
系统会对XQ1View(也就是XQ11View的父视图),发送一条hitTest消息, hitTest会调用pointInside方法.如果XQ1View的pointInside方法返回false则XQ1View的子视图不会被hitTest,XQ1View的hitTest将返回nil,本次hitTest就结束了.若返回true,那么系统将对XQ1View的子视图数组进行一个反向遍历的hitTest检测.因此首先检测的是XQ12View.由于用户实际点击的是XQ11View,所以XQ12View的pointInside会返回false,XQ12View的hitTest将返回nil.接下来将对XQ11View进行hitTest检测,hitTest会调用pointInside方法,而用户恰好点击的是XQ11View,因此pointInside方法返回true,由于XQ11View没有子视图了,因此XQ11View的hitTest将返回自己.hitTest检测结束.于是就找到了.XQ1View的hitTest也将返回XQ11View.可以推断出系统最先是对UIWindow进行hitTest检测的.因此UIWindow的hitTest也将返回XQ11View.于是系统就拿到了第一响应者XQ11View.拿到之后系统会尝试让XQ11View处理事件.接下来就是事件在响应者链中传递了.

如果点击XQ12View超出它父视图的那部分,也让它能够响应事件,该如何实现呢?

通过上面的分析,可以知道必须想办法让它的父视图的pointInside返回true.否则都不会对XQ12View进行hitTest检测.实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class XQ1View: XQView {

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rs = super.point(inside: point, with: event)

if !rs {
for subView in subviews.reversed() {
let subPoint = subView.convert(point, from: self)
if subView.bounds.contains(subPoint) {
return true
}
}
}

return rs
}

}

ps:对一个视图进行hitTest前,系统还会有很多判断比如会忽略隐藏(hidden=YES)的视图,禁止用户操作 (userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。这些视图都不会被hitTest检测.

事件是如何在响应者链中传递的

官方解释:
If the text field does not handle an event, UIKit sends the event to the text field’s parent UIView object, followed by the root view of the window. From the root view, the responder chain diverts to the owning view controller before returning to the view’s window.If the window does not handle the event, UIKit delivers the event to the UIApplication object, and possibly to the app delegate if that object is an instance of UIResponder and not already part of the responder chain.

当用户点击了text field,期间会经历那些过程呢?

首先系统会去找第一响应者,找的过程就是上面所述,这里就是text field.找到了第一响应者,那么事件将优先由第一响应者处理.如果text field不处理,那么UIKit会将事件发送给text field的父视图.如果它的父视图也不处理则事件将会沿着响应者链一直往上传递(如果View是作为viewController的根视图,那么该View的next responder将是UIViewController),当传递给UIWindow对象时,如果window也不处理,则事件将传递给UIApplication.UIApplication可能会传给app delegate,前提是app delegate是UIResponder实例,需要注意的是app delegate已经不是响应者链里的一部分了.

一般来说工程的app delegate都是UIResponder实例,所以当AppDelegate也不处理时,那么就再也没有机会处理了,事件将被系统丢弃.

期间只要有一个响应者对象处理了该事件.系统默认将不再传递给next responder,本次事件就算处理成功.

变更响应者链

You can alter the responder chain by overriding the nextResponder property of your responder objects. Many UIKit classes already override this property and return specific objects.

If you override the nextResponder property for any class, the next responder is the object you return.

几种类型对象的默认nextResponder.

  1. UIView

    If the view is the root view of a view controller, the next responder is the view controller.

    If the view is not the root view of a view controller, the next responder is the view’s superview.

  2. UIViewController

    If the view controller’s view is the root view of a window, the next responder is the window object.

    If the view controller was presented by another view controller, the next responder is the presenting view controller.

    (xq注:举例,UINavigationController的根视图控制器vcA点击按钮,跳转到vcB,那么vcB的nextResponder是UIViewControllerWrapperView->UINavigationTransitionView->UILayoutContainerView->UINavigationController,UINavigationController的根视图类型确实是UILayoutContainerView,忽略中间三个系统类,那么vcB的nextResponder就是UINavigationController.注意不是vcA)

  3. UIWindow. The window’s next responder is the application object.

  4. UIApplication. The app object’s next responder is the app delegate, but only if the app delegate is an instance of UIResponder and is not a view, view controller, or the app object itself.

如何把消息发送给next responder?

1
2
3
4
5
6
7
8
9
10
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];

if (touch.tapCount == 2) {
[[self nextResponder] touchesBegan:touches withEvent:event];//这里最好写[self nextResponder]而不是写super
return;
}
... Go on to handle touches that are not double taps
}
觉得文章有帮助可以打赏一下哦!