iOS 响应者链

概述

响应者链的作用就是让 APP 知道用户点击里了哪里 然后应该哪个控件做出反应
专业点说 响应者链就是由多个响应者组合起来的链条 就叫做响应者链 它表示了每个响应者之间的联系 并且可以使得一个事件可选择多个对象处理

UIResponder

我们的App与用户进行交互,基本上是依赖于各种各样的触发事件和运动事件。例如,用户点击界面上的按钮,我们需要触发一个按钮点击事件,并进行相应的处理,以给用户一个响应。UIView的三大职责之一就是处理触发事件和运动事件,一个视图是一个事件响应者,可以处理点击等触发事件,而这些触发事件和运动事件就是在UIResponder类中定义的。

一个UIResponder类为那些需要响应并处理事件的对象定义了一组接口。这些事件主要分为两类:触摸事件(touch events)和运动事件(motion events)。

在UIKit中,UIApplication、UIView、UIViewController这几个类都是直接继承自UIResponder类。因此UIKit中的视图、控件、视图控制器,以及我们自定义的视图及视图控制器都有响应事件的能力。这些对象通常被称为响应对象,或者是响应者

响应者链

大多数事件的分发都是依赖响应链的。响应链是由一系列链接在一起的响应者组成的。一般情况下,一条响应链开始于第一响应者,结束于application对象。如果一个响应者不能处理事件,则会将事件沿着响应链传到下一响应者。

确定第一响应者

当点击了屏幕会产生一个触摸事件,消息循环(runloop)会接收到触摸事件放到消息队列里,UIApplication 会从消息队列里取事件分发下去,接着需要找到去响应这个事件的最佳视图,也就是 Responder,所以开始的第一步应该是找到 Responder,那么又是如何找到的呢?那就不得不引出 UIView 的 2 个方法:

// 返回此次触摸事件初始点所在的视图
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 

// 返回视图是否包含指定的某个点
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 

通过在显示视图层级中依次对视图调用这个 2 个方法来确认该视图是不是能响应这个点击的点,首先会调用 hitTest,然后在 hitTest 中调用 pointInside,最终 hitTest 返回的那个 view 就是最终的响应者Responder。

在这里插入图片描述
以上图为例,假设点击了 View3。

首先调用 UIWindow 的 hitTest:withEvent:,在这个方法中调用 pointInside:withEvent: 方法判断点击是否在 UIWindow 的范围内,显然 pointInside 返回 YES;

然后遍历 window 的子视图,来到 RootView,调用 RootView 的 hitTestpointInside,因为点击发生在 RootView 中所以继续遍历它的子视图。

从 View2 开始的,调用 View2 的 hitTestpointInside,由于触摸点在 View2 的范围内,所以 pointInside 返回 YES;然后继续遍历 View2 的子视图,从 View4 开始,因为点击不发生在 View4, 所以 pointInside 返回 NO,而 View4 没有子视图,所以 hitTest 返回 null;然后继续在 View2 的另外一个子视图 View3 中调用 hitTestpointInside,因为点击的就是 View3 所以 pointInside 返回 YES,且 View3 没有子视图,所以 hitTest 返回了自己 View3;接着 View2 的 hitTest 也返回 View3,RootView 的 hitTest 也返回 View3,直到 UIWindow 也返回 View3,自此我们找到了响应视图:View3。

在这里插入图片描述

  • 寻找事件的响应视图是通过调用视图的hitTestpointInside完成的
  • hitTest的调用顺序是从UIWindow开始 对视图的每个子视图依次调用
  • 遍历直到找到响应视图 然后逐级返回最终到UIWindow返回此视图 就算还有没有遍历到的视图 也不会再去遍历了

nextResponder

所有继承 UIResponder 的控件都有一个 nextResponder 属性,此属性会返回在 Responder Chain 中的下一个事件处理者,如果每个 Responder 都不处理事件,那么事件将会被丢弃。所以继承自 UIResponder 的子类便会构成一条响应者链,所以我们可以打印下以 View3 为开始的响应者链是什么样的:

在这里插入图片描述

可以看到响应者链一直延伸到 AppDelegate,View3 的下一个是 View2,也就是 View3 的父视图,View2 下一个是 RootView,也是父视图,而 RootView 的下一个则是 Controller。

所以下一个响应者的规则是:如果有父视图,则 nextResponder 指向父视图,如果是控制器根视图则指向控制器,控制器如果在导航控制器中,则指向导航控制器的相关显示视图,最后指向导航控制器,如果是根控制器则指向 UIWindow,UIWindow 的 nextResponder 指向 UIApplication,最后指向 AppDelegate,而他们实现这一套指向都是靠重写 nextReponder 实现的。

当点击了 View3,先是由 UIWindow 通过 hitTest 返回所找到响应者 View3;接着会执行 View3 的 touchesBegan,然后是通过 nextResponder 依次是 View2、RootView,完全是按照 nextResponder 链条的调用顺序,touchesEnded 也是同样的顺序。

在这里插入图片描述
这是View3没有处理点击事件的情况

添加了点击事件之后情况如下

在这里插入图片描述

可以看到 touchesBegan 顺着 nextResponder 链条调用了,但是 View3 处理了事件,去执行了相关事件处理方法,而 touchesEnded 并没有得到调用。

  • 找到最适合的响应视图后事件会从视图开始沿着响应链传递 直到找到处理事件的视图 如果没有找到处理事件的视图 这个事件会被丢弃

无法点击的事件

  • alpha = 0、子视图的 frame 超出父视图、userInteractionEnabled = NO、hidden = YES 这些情况下,视图会被忽略,不会调用 hitTest。
  • 若父视图被忽略,后其所有子视图也会被忽略。换句话说,若父视图不可点击,则其子视图一定不能点击。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

waxuuuu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值