UIResponder响应链

一、概述
  UIView与UIViewController的共同父类:UIResponder,对于点击touches一系列方法,UIView与UIViewController会做出一系列反应,下面从“如何找到点击的子view”和“如何根据响应链响应”两方面来认识UIResponder。
 
二、 如何找到点击的子view
  当用户点击某一个视图或者按钮的时候会首先响应application中UIWindow一层一层的向下查找,直到找到用户指定的view为止。
  比如上图,点击ViewE,会首先响应application中UIWindow一层一层的向下查找。查到ViewA,发现点击位置在ViewA内,接下来发现点击位置在ViewB内,接下来发现点击位置在ViewE内,就找到了ViewE。主要通过下面两个方法来找到的:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;   // default returns YES if point is in bounds

UIWindow会通过调用hitTest:withEvent:方法(这个方法会对UIWindow的所有子view调用pointInside:withEvent:方法,其中返回的YES的视图为ViewA,得知用户点击的范围在ViewA中),类似地,在ViewA中调用hitTest:withEvent:方法,得知用户点击的范围在ViewB中,依此类推,最终找到点击的视图为ViewE。

其中,hitTest:withEvent:方法大致的实现如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
     for (UIView *view in self.subviews) {
          if([view pointInside:point withEvent:event]){
              UIView *hitTestView = [view hitTest:point withEvent:event];
             if(nil == hitTestView){
                  return view;
              }
          }
      }
     return nil;
 }

通过以上递归的形式就能找到用户点击的是哪个view,其中还要注意的是当前的view是否开启了userIntercationEnabled属性,如果这个属性未开启,以上递归也会在未开启userIntercationEnabled属性的view层终止。

 

三、如何根据响应链响应

  既然找到了用户点击的view,那么当前就应该响应用户的点击事件了,UIView与UIViewController的共同父类是UIResponder,他们都可以复写下列4个方法:

-(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;

  这个响应点击事件的过程是上面的逆序操作,这就是用到了UIResponder的nextResponder方法了。比如上面的图点击ViewE,这时候ViewE先响应,接下来是nextResponder即ViewB,接下来是ViewB的nextResponder即ViewA。

  关于nextResponder有如下几条规则:

1. 当一个view调用其nextResponder会返回其superView;
2. 如果当前的view为UIViewController的view被添加到其他view上,那么调用nextResponder会返回当前的UIViewController,而这个UIViewController的nextResponder为view的superView;
3. 如果当前的UIViewController的view没有添加到任何其他view上,当前的UIViewController的nextResponder为nil,不管它是keyWinodw或UINavigationController的rootViewController,都是如此;
4. 如果当前application的keyWindow的rootViewController为UINavigationController(或UITabViewController),那么通过调用UINavigationController(或UITabViewController)的nextResponder得到keyWinodw;
5. keyWinodw的nextResponder为UIApplication,UIApplication的nextResponder为AppDelegate,AppDelegate的nextResponder为nil。

用图来表示,如下所示:

 

四、遇到的问题

  在开发过程中,我们有可能遇到UIScrollView 或 UIImageView 截获touch事件,导致touchesBegan: withEvent:/touchesMoved: withEvent:/touchesEnded: withEvent: 等方法不执行。比如下面这种情况,scrollView的superView是view,view对应的viewController中的touchesBegan: withEvent:/touchesMoved: withEvent:/touchesEnded: withEvent: 等方法就不执行。

如果想让viewController中的方法执行的话,你可能会提出下面的解决办法:
@implementation UIScrollView (Touch)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if([self isMemberOfClass:[UIScrollView class]]) {
        [[self nextResponder] touchesBegan:touches withEvent:event];
    }
}
@end

这样UIScrollView确实会用nextResponder把响应传递到view,接下来传递到viewController中。但是,如果没有使用if([self isMemberOfClass:[UIScrollView class]]) 进行过滤判断,那么,有可能会导致一个使用系统手写输入法时带来的crash问题。即手写的键盘的子view是UIKBCandidateCollectionView,调用了[[self nextResponder] touchesBegan:touches withEvent:event];后会造成系统的crash问题:

-[UIKBBlurredKeyView candidateList]: unrecognized selector sent to instance 0x104f6c6b0

这个crash的复现见《UIKBBlurredKeyView candidateList:unrecognized...BUG修复》,它的解决办法也随处可见,比如《-[UIKBBlurredKeyView candidateList]: unrecognized selector sent to instance 0x5a89960

此crash的技术层面详细原因:

手写的键盘的子view是UIKBCandidateCollectionView(UIColloectionView的子类)的实例,它的nextResponder是UIKBHandwritingCandidateView类型的实例,执行UIKBHandwritingCandidateView的touchesBegan:withEvent:方法后,会使得整个candidate view呈选中状态,而苹果对手写键盘的选择candidate字符时的原生处理方法是会避免candidate view呈选中状态的。整个candidate view呈选中状态后后再点击键盘的任意地方,本应调用UIKBCandidateView实例的方法candidateList,结果调用了UIKBBlurredKeyView的candidateList方法,导致方法找不到,导致"-[UIKBBlurredKeyView candidateList]: unrecognized selector sent to instance "crash。

crash总结:

通过对这个crash的详细分析,虽然上面的使用isMemberOfClass判断后使用nextResponder对事件响应链进行传递没有问题,但由于nextResponder依然具有不可控性,还是不建议用category复写系统的方法,这一点以后一定注意。

谨慎使用Category,特别是覆盖系统原始方法的category的实现。

 (PS:本文参考文章《关于响应者链》)

写在最后

以下三个按优先级排:

1. UIKit的target-action方法(target-action 方法是在手势关联的方法中识别出来的。当点击按钮时,按钮的 target-action 方法会触发,手势的方法会被忽略。)

2. 手势识别(和响应者链同时进行,但是识别某个手势后,系统给该视图的响应链发送了 touchCancelled 的信息,从而阻止这个 UITouch 继续触发这个视图的 touches 系列方法(同时也取消了别的相关手势的 touches 系列方法,图中未体现)。在这之后,被调用的只有与手势关联的 target-action 方法)

3. 响应者链。   

具体参见《iOS | 响应链及手势识别

posted @ 2017-07-10 22:21  Xylophone  Views(1446)  Comments(0Edit  收藏  举报