iOS事件分发机制(二)The Responder Chain

704 查看

响应链简单来说,就是一系列的相互关联的对象,从firstResponder开始,到application对象结束,如果firstResponder 无法响应事件,则交给nextResponder来处理,直到结束为止。iOS中很多类型的事件分发,都依赖于响应链;在响应链中,所有对象的基类都是UIResponder,也就是说所有能响应事件的类都是UIResponder的子类,UIApplication/UIView/UIViewController都是UIResponder的子类,这说明所有的Views,绝大部分Controllers(不用来管理View的Controller除外)都可以响应事件。

PS.CALayer 不是UIResponder的子类,这说明CALayer无法响应事件,这也是UIView和CALayer的重要区别之一。

上一篇文章讲解到iOS中TouchEvent分发的第一步(如果您还没有了解,可以参考上一篇博客:iOS事件分发机制(一)),确定HitTestView,并且文章中也说明了,不管你的application当前设置是否忽略交互事件(application.beginIgnoringInteractionEvents) ,hitTest总会调用的,当然如果忽略了交互事件,之后的事件分发都不会调用了,事件会直接被废弃掉。假定我们没有忽略事件,如果hitTestView 无法处理这个事件,事件就通过响应链往上传递(hitTestView算是最早的Responder),直到找到一个可以处理的Responder为止。举个例子,如果触摸通过hitTest确定的是一个View,而这个View没有处理事件,则事件会发送给nextResponder 去处理,通常是superView,有关nextResponder的事件传递过程,官方给出了一张很形象的图,如下所示

PS.View处理事件的方式有手势或者重写touchesEvent方法或者利用系统封装好的组件(UIControls)。

图中所表示的正是nextResponder的查找过程,两种方式分别对应两种app的架构,左边的那种app架构比较简单,只有一个VC,右边的稍微复杂一些,但是寻找路线的原则是一样的,先解释一下,UIResponder本身是不会去存储或者设置nextResponder的,所谓的nextResponder都是子类去实现的(这里说的是UIView,UIViewController,UIApplication),关于nextResponder的值总结如下:

1、UIView的nextResponder是直接管理它的UIViewController(也就是VC.view.nextResponder=VC),如果当前View不是ViewController直接管理的View,则nextResponder是它的superView(view.nextResponder = view.superView)

2、UIViewController的nextResponder是它直接管理的View的superView(VC.nextResponder = VC.view.superView)

3、UIWindow的nextResponder是UIApplication

4、UIApplication的nextResponder是UIApplicationDelegate(官方文档说是nil)

我写了一段代码,打印当前UIResponder的所有nextResponder,大家可以拿去试一下,代码很简单,如下:

然后我测试了一下,打印的日志如下图所示:

这样比较清晰,大家也会直观的看到nextResponder的查找过程。

同样,我们也得到了一个方法,去得到当前任意View的ViewController,实现思路就是 不断的去找nextResponder,直到找到UIViewController为止(如果有一个的话),直接上代码。

一般来说,我不太习惯在View中直接获取VC或者NVC,我习惯代理的方式,但是做项目的过程中,不同Team的习惯不同,毕竟做项目和做研究还不是同一回事(以前的项目就是在任何地方你都可以通过当前View去获取最近的NAV然后PushVC),所以大家用这个的时候可以综合当前的编码规范,大家可以拿上面的东西作为参考,如果写的有不对的地方,可以指出。

接下来我们说正事了,假定我们现在有一个View是hitTestView,命名为 STImageView, 现在我们想让这个image处理一些事情,比如所有的图片点下之后加一个灰色的效果,我们就把事件分发给它。

在UIResponder中,提供以下几个方法,几个方法分别表示点击的不同状态,大家看名字就能明白差不多:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

如果我们想让我们当前的Responder处理事件,我们则需要重写如下的几个方法。我们的需求是手指按下图片的时候加一个灰色的效果,松开的时候灰色消失。关于灰色的实现,我们暂定用一个View贴在ImageView上named maskView,然后用hidden来控制是否显示(上一篇文章有说过,所有hidden的View默认不接受任何事件)。

我们需要在

touchesBegan方法里面self.maskView.hidden = NO;

然后在

touchesEnded/Cancelled 里面 self.maskView.hidden = YES;

就可以实现我们的效果了,原理很简单,我们的hitTestView 在事件分发的时候去处理事件,仅此而已。这里注意一下:UIImageView的默认是不接受点击事件的,如果想要实现如上所示效果,需要设置userInteractionEnabled=YES;

说到这里,就有人产生了疑问,如果这么实现的话,那如果本身UIImageView还想让下面的View处理事件该怎么办?会不会把所有的事件拦截下来?这里就说到了另一个问题,UIResponder在知道需要处理事件的时候,还是有决定权的,比如我可以决定让整个响应链继续走下去,或者直接中断掉整个响应链。如果中断了响应链,那么所有在链上的nextResponder都不会得知有事件发生,iOS也提供了这个方法,其实很简单:

我们在重写TouchesEvents的时候,如果不想让响应链继续传递,就不调用super对应的实现就可以了,相反,有些时候你只需要做一个小改变,如上所示,但是你不想中断响应链,你就需要调用父类对应的实现。

这里有一点需要注意,一般来说,我们如果想要自己处理一些事件,我们需要重写如上所示的方法,如果我们想自己处理,就不需要调用super。调用super的目的就是为了把事件传递给nextResponder,并且如果我们在 touchesBegan 中没有调用super,则super不会响应其他的回掉(touchesMoved/touchesEnded),但是我们需要重写所有如上所示的方法来确保我们的一切正常。touchesBegan 和 touchesEnded/touchesCancelled一定是成对出现的,这点大家可以放心。

有关触摸事件在响应链上的分发,就差不多这么多东西,最重要的是大家可以看那几个touches方法,多做实验,就可以了解的更加深入。

这里有一些补充,响应链能够处理很多东西,不仅仅是触摸事件。一般来说,如果我们需要一个对象去处理一个非触摸事件(摇一摇,RemoteControlEvents,调用系统的复制、粘贴框等),我们要确保该对象是UIResponder子类,如果我们要接收到事件的话,我们需要做两件事情

1、重写canBecomeFirstResponder ,并且返回YES

2、在需要的时候像该对象发送becomeFirstResponder消息。

我们有时候会遇到一些问题,比如我们重写了motionEvents,但是我们不能收到摇一摇的回调,或者我们的UIMenuController老是不弹出,我们就需要检查一下,我们是否满足了如上所示的条件,而且要确保becomeFirstResponder的发送时机正确。

当然,这个补充对于触摸事件无效,触摸事件的第一响应者是根据hitTest确定而来的,有点绕,需要仔细捋捋。

需要注意的是:

如果你自己想自定义一个非TouchEvent的事件,当需要继续传递事件的话,切记不要在实现内直接显示的调用nextResponder的对应方法, 而是直接调用super对应的方法来让这个事件继续分发到响应链。

到目前为止,事件的分发还没有结束,之后会有一篇文章介绍一个很重要的角色,手势。

最后,附上官方的文档

Event Handling Guide for iOS