前言
当咱们点击屏幕的时分,就产生了一个点击事情,那程序是如何知道这个点击事情应该由谁来处理?
呼应者
在iOS中,能够呼应事情的便是呼应者,而所有呼应者都是UIResponser的子类,例如:UIView、UIButton、UIControl、UIWindow、UIViewController、AppDelegate、UIApplication以及它们的子类。
UIResponder声明了各种点击事情的处理,比方点击,按压,移动等。
呼应链
呼应链便是呼应者都衔接一同的一个链条的层级关系,说是链条其实更相似树结构。
这个链条从程序开始运转时就树立并不断将呼应者链接进来。
咱们都知道程序运转后,UIApplication会生成一个单例,并与AppDelegate进行相关。而AppDelegate就作为整个呼应链的根树立存在,接着UIApplication的单例就会作为呼应者链接在根上,即[UIApplication sharedApplication].nextResponser = AppDelegate。
当任何一个UIWindow被创立时,UIWindow都会主动链接在UIApplication的单例上,即把UIWindow的nextResponser设置为UIApplication的单例。
当UIWindow设置rootViewController时,rootViewController就会链接在UIWindow上。
UIViewController初始化loadView时, UIViewController的view就会链接在UIViewController上。
addSubView的操作进程中,subView的nextResponser会被设置为superView。
举例验证环节:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UIView *view = [[UIView alloc] init];
[self.view addSubview:view];
UILabel *label = [[UILabel alloc] init];
[view addSubview:label];
[self logOutResponderChain:label];
}
- (void)logOutResponderChain:(UIResponder *)responder {
UIResponder *nextResponder = responder.nextResponder;
NSLog(@"%@ -> ", NSStringFromClass([responder class]));
while (nextResponder) {
NSLog(@"%@ -> ", NSStringFromClass([nextResponder class]));
nextResponder = nextResponder.nextResponder;
}
NSLog(@"*");
}
打印成果如下:
UILabel -> UIView -> UIView -> ViewController ->
UIDropShadowView -> UITransitionView ->
UIWindow -> UIWindowScene -> UIApplication -> AppDelegate -> \*
传递链
通过上文,咱们现已知道了呼应链是如何树立的,而树立呼应链便是为了让事情能找到对应的处理者,而找到处理者的进程称之为事情的传递链。
这儿先给出结论:
(1)当用户点击屏幕触发事情,体系硬件进程会获取到这个事情,将事情简略处理封装后存到体系中,体系接着将事情转交到UIApplication办理的事情队列中。(这一部分涉及RunLoop和端口通讯) (2)UIApplication会从事情队列中取出最前面的事情,并将事情顺着呼应链分发下去,寻找适宜的控件进行处理。 (3)依据呼应链,UIApplication会先发送事情给主窗口keyWindow, (4)keyWindow再依据呼应链逐级分发下去,直到找到适宜的处理控件停止。 (5)找到适宜的处理控件后,keyWindow就会调用该控件中适宜的事情办法来处理事情。 (6)如果找不到适宜的处理控件,该事情就会被抛弃。
那么怎样判别哪个控件是事情的最适宜的处理者?
(1)控件肯定要显示在屏幕上,用户要能看见才会去触发事情。 (2)控件能呼应点击事情。 (3)点击事情产生在控件规模内。 (4)该控件没有子控件能够处理事情。
事情是怎样逐级分发下去的?
通过调用下面两个UIView的办法进行事情的分发。
// 判别事情触发点是否在这个View内部
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
// 依据上一个办法的成果,遍历View的subViews
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
这两个办法是如何进行传递的,进行验证环节:
我首要创立三个继承UIView的子类AView、BView、CView,然后都重写pointInside:withEvent:、hitTest:withEvent:和touchesBegan:withEvent:办法,进行打印。
//注:AView、BViewh和CView都一样,就不重复粘贴了。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"AView --- hitTest");
UIView *view = [super hitTest:point withEvent:event];
NSLog(@"AView --- hitTest --- return %@", view.class);
return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"AView --- pointInside");
return [super pointInside:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"AView --- touchesBegan");
}
然后增加到UIViewController的view上。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
AView *a = [[AView alloc] initWithFrame:CGRectMake(100, 100, 300, 300)];
a.backgroundColor = [UIColor blueColor];
[self.view addSubview:a];
BView *b = [[BView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
b.backgroundColor = [UIColor greenColor];
[a addSubview:b];
CView *c = [[CView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
c.backgroundColor = [UIColor grayColor];
[b addSubview:c];
}
首要咱们在CView的pointInside:withEvent:中打个断点,然后点击CView:
检查调用栈,pointInside:withEvent:是在hitTest:withEvent:中调用的,只要回来Yes才会去遍历子控件。
接着放开断点,事情传递链打印如下:
AView --- hitTest
AView --- pointInside --- Yes
BView --- hitTest
BView --- pointInside --- Yes
CView --- hitTest
CView --- pointInside --- Yes
CView --- hitTest --- return CView
BView --- hitTest --- return CView
AView --- hitTest --- return CView
CView --- touchesBegan
由于CView没有子控件,且点击在CView上,所以回来CView作为事情处理者,来调用touchesBegan:withEvent:。
接着点击BView但不在CView上,事情传递链打印如下:
AView --- hitTest
AView --- pointInside --- Yes
BView --- hitTest
BView --- pointInside --- Yes
CView --- hitTest
CView --- pointInside --- No
CView --- hitTest --- return (null)
BView --- hitTest --- return BView
AView --- hitTest --- return BView
BView --- touchesBegan
由于点击不在CView上所以回来了(null),而点击又在BView上,所以回来BView作为事情处理者,来调用touchesBegan:withEvent:。
通过上面的测验,咱们现已证明了判别条件(3)点击事情产生在控件规模内和(4)该控件没有子控件能够处理事情的正确性,然后来看一下(1)和(2)。
对CView别离进行设置然后测验:
c.userInteractionEnabled = NO;
c.hidden = YES;
c.alpha = 0;
三次事情,打印成果共同如下:
AView --- hitTest
AView --- pointInside --- Yes
BView --- hitTest
BView --- pointInside --- Yes
CView --- hitTest
CView --- hitTest --- return (null)
BView --- hitTest --- return BView
AView --- hitTest --- return BView
BView --- touchesBegan
当CView未显示在屏幕上或不能呼应点击事情时,hitTest:withEvent:都不调用pointInside:withEvent:直接回来了(null)。虽然点击在CView上,但CView不能呼应事情,因而由CView的上一级BView来处理。
通过上面的验证进程,咱们能够得到UIView中hitTest:withEvent:大概的内部完成:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.alpha == 0 ||
self.hidden == YES ||
self.userInteractionEnabled == NO) {
return nil;
}
if (![self pointInside:point withEvent:event]) {
return nil;
}
UIView *eventView = nil;
for (UIView *subView in self.subviews) {
//以子视图为参考系转化坐标点,漏这一步 pointInside:withEvent:会出错,事情就传递不下去了。
CGPoint subPoint = [self convertPoint:point toView:subView];
eventView = [subView hitTest:subPoint withEvent:event];
}
if (!eventView) {
eventView = self;
}
return eventView;
}
传递链的简略总结
用户产生接触事情 -> 事情进入UIApplication事情队列 -> UIApplication分发事情给keyWindow -> [UIWindow hitTest:withEvent:] -> [UIView hitTest:withEvent:] -> … -> [UIView hitTest:withEvent:] -> 回来最适宜的view给keyWindow -> [UIWindow _sendTouchesForEvent:] -> [UIView touchesBegan:withEvent:]。
传递链面试问题
问:父视图设置为不可点击,子视图为什么也不能够点击?
答:当父视图设置为不可点击,传递链在父视图就被到回来为(null)了,都传递不到子视图。
问:事情传递链和事情呼应链差异?
答:事情传递链是从父控件到子控件传递,从上到下。事情呼应链是从子控件到父控件进行呼应、链接,从下到上。
问:子视图在父视图之外显示的区域,点击是否有效?
答:虽然默许会显示,但是点击是无效的,父视图的pointInside:withEvent:判别通不过。
传递链使用
实战使用:到传递
如果咱们不想让视图呼应事情,除了userInteractionEnabled = NO,还能够重载pointInside:withEvent:办法,一直回来NO就行了。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return NO;
}
实战使用:子视图和父视图一起处理一个事情
子视图需求重载touch办法来处理事情,只需求再调用[super touch]将事情传递给父视图即可:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//子视图处理该事情
NSLog(@"%s",**func**);
//再调用 super 让父视图也处理该事情
[super touchesBegan:touches withEvent:event];
}
实战使用:隔层传递
假定vc.view上增加AView,AView增加了BView,BView又增加了CView。
点击CView能够呼应处理事情,点击BView却不呼应,而AView却能够呼应处理事情,跳过中间层进行呼应事情,也便是隔层传递。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
BOOL value = (CGRectContainsPoint(frame, point));
NSArray *views = [self subviews];
for (UIView *subview in views) {
value = (CGRectContainsPoint(subview.frame, point));
if (value) {
return value;
}
}
return NO;
}
实战使用:扩展控件的呼应规模
咱们能够重载pointInside:withEvent:办法,将控件的判别规模变大就能够了:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect relativeFrame = self.bounds;
UIEdgeInsets hitTestEdgeInsets = UIEdgeInsetsMake(-15, -15, -15, -15);
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
实战使用:深层级 View 之间的通讯
假定vc.view上增加AView,AView增加了BView,BView又增加了CView。
当CView有一个事情需求vc来处理,这个时分如果用 block、delegate、Notification都会比较费事,这个时分就能够通过呼应者链,将音讯传递上去。
首要咱们为UIResponder写个分类办法,相似Router办法,让整个呼应链传递这个办法,CView 只需求调用该办法,让控制器去重写该办法就能够和CView进行通讯了。
//UIResponder 分类完成
/**
发送一个路由器音讯, 对eventName感兴趣的 UIResponsder 能够对音讯进行处理
@param eventName 产生的事情名称。
@param userInfo 传递音讯时, 携带的数据。
*/
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSObject *)userInfo {
[[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}
//CView调用
[self routerEventWithName:@"CViewEvent" userInfo:nil];
//控制器重载
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSObject *)userInfo {
NSLog(@"%s eventName:%@",**func**,eventName);
}