一、概述
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的view为ViewA,得知用户点击的范围在ViewA中),类似地,在ViewA中调用hitTest:withEvent:方法,得知用户点击的范围在ViewB中,依此类推,最终找到点击的view为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的实现。