响应链简单来说,就是一系列的相互关联的对象,从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,大家可以拿去试一下,代码很简单,如下:
1 2 3 4 5 6 7 8 9 |
void STLogResponderChain(UIResponder *responder) { NSLog(@"------------------The Responder Chain------------------"); NSMutableString *spaces = [NSMutableString stringWithCapacity:4]; while (responder) { NSLog(@"%@%@", spaces, responder.class); responder = responder.nextResponder; [spaces appendString:@"----"]; } } |
然后我测试了一下,打印的日志如下图所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
UIButton ----STPView --------UIView ------------STPFeedViewController ----------------UIView --------------------UIView ------------------------_STWrapperViewController ----------------------------UIView --------------------------------UIView ------------------------------------STNavigationController ----------------------------------------STPWindow --------------------------------------------UIApplication ------------------------------------------------STPAppDelegate |
这样比较清晰,大家也会直观的看到nextResponder的查找过程。
同样,我们也得到了一个方法,去得到当前任意View的ViewController,实现思路就是 不断的去找nextResponder,直到找到UIViewController为止(如果有一个的话),直接上代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
@interface UIView (STKit) /** * @abstract view's viewController if the view has one */ - (UIViewController *)viewController; /** * @abstract 递归查找view的nextResponder,直到找到类型为class的Responder * * @param class nextResponder 的 class * @return 第一个满足类型为class的UIResponder */ - (UIResponder *)nextResponderWithClass:(Class) class; /// 查找firstResponder - (UIResponder *)findFirstResponder; @end @implementation UIView (STKit) /** * @abstract view's viewController if the view has one */ - (UIViewController *)viewController { return (UIViewController *)[self nextResponderWithClass:UIViewController.class]; } /** * @abstract 递归查找view的nextResponder,直到找到类型为class的Responder * * @param class nextResponder 的 class * @return 第一个满足类型为class的UIResponder */ - (UIResponder *)nextResponderWithClass:(Class) class { UIResponder *nextResponder = self; while (nextResponder) { nextResponder = nextResponder.nextResponder; if ([nextResponder isKindOfClass:class]) { return nextResponder; } } return nil; } - (UIResponder *)findFirstResponder { if (self.isFirstResponder) { return self; } for (UIView *subView in self.subviews) { id responder = [subView findFirstResponder]; if (responder) { return responder; } } return nil; } @end |
一般来说,我不太习惯在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对应的方法来让这个事件继续分发到响应链。
到目前为止,事件的分发还没有结束,之后会有一篇文章介绍一个很重要的角色,手势。
最后,附上官方的文档