文章目录
- 什么是响应者链
- 响应者 Responder
- 方法
- 实例讲解
- 总结
- 响应者链
- nextResponder
- 响应者栈
- 点击事件
- OrangeView处理点击事件
- OrangeView不处理点击事件
- 总结
- 无法响应的情况
- 参考文献
- GitHub
之前讲过响应者链的概念、程序和作用,但感觉有点枯燥,不太好理解。这篇博客用一个小demo来讲解一下
方法借鉴自:
什么是响应者链
iOS响应者链(Responder Chain)是支撑App界面交互的重要基础,点击、滑动、旋转、摇晃等都离不开其背后的响应者链,所以每个iOS开发人员都应该彻底掌握响应者链的响应逻辑,本文旨在通过demo测试的方式展现响应者链的具体响应过程,帮助读者彻底掌握响应者链。
响应者 Responder
方法
在iOS中,能够响应事件的对象都是UIResponder的子类对象。UIResponder提供了四个用户点击的回调方法,分别对应用户点击开始、移动、点击结束以及取消点击,其中只有在程序强制退出或者来电时,取消点击事件才会调用。
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
而我们找到Responder需要用到UIView的两个方法:
//搜索是在哪个视图
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
//判断是否在这个视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
实例讲解
它具体能做什么呢,又是怎么确定对调用哪个View呢?我们看看这个多重视图
加入几个view:
- (void)viewDidLoad {
[super viewDidLoad];
TAYView *whiteView = [[TAYView alloc] init];
[self.view addSubview:whiteView];
whiteView.backgroundColor = [UIColor whiteColor];
whiteView.frame = self.view.bounds;
RedView *redView = [[RedView alloc] init];
[whiteView addSubview:redView];
redView.backgroundColor = [UIColor redColor];
redView.frame = CGRectMake(50, 150, 320, 100);
BlueView *blueView = [[BlueView alloc] init];
[whiteView addSubview:blueView];
blueView.backgroundColor = [UIColor blueColor];
blueView.frame = CGRectMake(50, 300, 320, 300);
OrangeView *orangeView = [[OrangeView alloc] init];
[blueView addSubview:orangeView];
orangeView.backgroundColor = [UIColor orangeColor];
orangeView.frame = CGRectMake(20, 30, 280, 100);
YellowView *yellowView = [[YellowView alloc] init];
[blueView addSubview:yellowView];
yellowView.backgroundColor = [UIColor yellowColor];
yellowView.frame = CGRectMake(20, 160, 280, 100);
}
WhiteView是最底部的View,RedView和BlueView都加在WhiteView上,OrangeView和YellowView都加在BlueView上
我们使用Runtime的运行时方法交换找到Responder需要用到UIView的两个方法,加入打印观察是否经过此方法
因为上面的视图都继承自UIView,所以我们创建一个UIView的category去在里面交换方法
#import "UIView+ChangeMethod.h"
#import <objc/runtime.h>
@implementation UIView (ChangeMethod)
+ (void)load {
Method origin = class_getInstanceMethod([UIView class], @selector(hitTest:withEvent:));
Method custom = class_getInstanceMethod([UIView class], @selector(tay_hitTest:withEvent:));
method_exchangeImplementations(origin, custom);
origin = class_getInstanceMethod([UIView class], @selector(pointInside:withEvent:));
custom = class_getInstanceMethod([UIView class], @selector(tay_pointInside:withEvent:));
method_exchangeImplementations(origin, custom);
}
- (UIView *)tay_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ hitTest", NSStringFromClass([self class]));
UIView *result = [self tay_hitTest:point withEvent:event];
NSLog(@"%@ hitTest return: %@", NSStringFromClass([self class]), NSStringFromClass([result class]));
return result;
}
- (BOOL)tay_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ pointInside", NSStringFromClass([self class]));
BOOL result = [self tay_pointInside:point withEvent:event];
NSLog(@"%@ pointInside return: %@", NSStringFromClass([self class]), result ? @"YES":@"NO");
return result;
}
现在点击YellowView看看打印情况:
可以看到,先从最底部的UIWindow开始,逐层向上调用。调用方法的顺序是先hitTest,再pointInside
打印情况来看并没有调用RedView的那两个方法,说明不响应该事件的view不会被调用还是遍历调用只要有return YES的就不继续同层遍历查找了呢,这次点击RedView看会不会调用BlueView的方法
可以得知,访问的顺序是遍历访问的。
那么我们又发现是先调用的BlueView方法才调用的RedView方法,为了更好的证明初始化和调用的顺序,我们交换一下两者的初始化顺序:
- (void)viewDidLoad {
[super viewDidLoad];
TAYView *whiteView = [[TAYView alloc] init];
[self.view addSubview:whiteView];
whiteView.backgroundColor = [UIColor whiteColor];
whiteView.frame = self.view.bounds;
// BlueView先初始化
BlueView *blueView = [[BlueView alloc] init];
[whiteView addSubview:blueView];
blueView.backgroundColor = [UIColor blueColor];
blueView.frame = CGRectMake(50, 300, 320, 300);
RedView *redView = [[RedView alloc] init];
[whiteView addSubview:redView];
redView.backgroundColor = [UIColor redColor];
redView.frame = CGRectMake(50, 150, 320, 100);
OrangeView *orangeView = [[OrangeView alloc] init];
[blueView addSubview:orangeView];
orangeView.backgroundColor = [UIColor orangeColor];
orangeView.frame = CGRectMake(20, 30, 280, 100);
YellowView *yellowView = [[YellowView alloc] init];
[blueView addSubview:yellowView];
yellowView.backgroundColor = [UIColor yellowColor];
yellowView.frame = CGRectMake(20, 160, 280, 100);
}
再点击RedView看会不会调用BlueView的方法
没有调用BlueView方法,即就是先加入的后调用
即为:
访问顺序从下到上,从左到右,如果hitTest: 返回值为YES就继续向上遍历,如果为NO就先同级遍历后向上遍历
返回时是逐级返回,从上到下
总结
- 寻找事件的最佳响应视图是通过对视图调用hitTest和pointInside完成的
- hitTest的调用顺序是从UIWindow开始,对视图的每个子视图依次调用,子视图的调用顺序是从后面加入的视图开始,到前面加入的视图
- 遍历直到找到响应视图,然后逐级返回最终到UIWindow返回此视图
响应者链
nextResponder
从上文中我们大概已经可以了解响应者链的顺序了,其实对于UIResponder存在一个nextResponder属性,此属性会返回在响应者链中的下一个事件处理者,如果每个Responder都不处理事件,那么事件将会被丢弃。
我们先打印一下YellowView的响应者链长什么样:
这里我为了可以在 viewDidAppear: 里调用,将YellowView设置为属性了
@interface FindResponderViewController ()
@property (nonatomic, strong) YellowView *yellowView;
@end
@implementation FindResponderViewController
- (void)viewDidLoad {
[super viewDidLoad];
TAYView *whiteView = [[TAYView alloc] init];
[self.view addSubview:whiteView];
whiteView.backgroundColor = [UIColor whiteColor];
whiteView.frame = self.view.bounds;
BlueView *blueView = [[BlueView alloc] init];
[whiteView addSubview:blueView];
blueView.backgroundColor = [UIColor blueColor];
blueView.frame = CGRectMake(50, 300, 320, 300);
RedView *redView = [[RedView alloc] init];
[whiteView addSubview:redView];
redView.backgroundColor = [UIColor redColor];
redView.frame = CGRectMake(50, 150, 320, 100);
OrangeView *orangeView = [[OrangeView alloc] init];
[blueView addSubview:orangeView];
orangeView.backgroundColor = [UIColor orangeColor];
orangeView.frame = CGRectMake(20, 30, 280, 100);
self.yellowView = [[YellowView alloc] init];
[blueView addSubview:_yellowView];
_yellowView.backgroundColor = [UIColor yellowColor];
_yellowView.frame = CGRectMake(20, 160, 280, 100);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UIResponder *nextResponder = _yellowView.nextResponder;
NSMutableString *pre = [NSMutableString stringWithString:@"--"];
NSLog(@"YellowView");
while (nextResponder) {
NSLog(@"%@%@", pre, NSStringFromClass([nextResponder class]));
[pre appendString:@"--"];
nextResponder = nextResponder.nextResponder;
}
}
我们可以看到,nextResponder的规则为:
- 如果有父视图则nextResponder指向父视图
- 如果是控制器根视图则指向控制器
- 控制器如果在导航控制器中则指向导航控制器的相关显示视图最后指向导航控制器(我这个demo中没有导航控制器所以没有打印)
- 如果是根控制器则指向UIWindow,UIWindow的nextResponder指向UIApplication最后指向AppDelegate
响应者栈
我们根据上文的hitTest响应顺序和nextResponder顺序可以大概推测出有一个响应者栈:将可以响应(即pointInside: withEvent:为YES)的视图入栈,最后需要执行事件时再从栈中取视图,最上面的视图最先处理,如果无法处理则给下一个视图,直到AppDelegate如果都无法处理,则事件被摒弃
大概是这个样子:
对比 pointInside: withEvent: 和nextResponder我们也可以发现,ViewController是一个例外,虽然它不是视图不走 pointInside: withEvent: ,但是也被加入了响应者栈
点击事件
OrangeView处理点击事件
我们来通过点击事件验证一下响应者栈
先给OrangeView添加一个点击事件,为了顺便可以看看touchesBegin那些方法的调用顺序,我在UIView分类和FindResponderViewController里也加入了方法打印
// 添加方法打印
@implementation UIView (ChangeMethod)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ touchesBegan", NSStringFromClass([self class]));
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ touchesMoved", NSStringFromClass([self class]));
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ touchesEnded", NSStringFromClass([self class]));
[super touchesEnded:touches withEvent:event];
}
// FindResponderViewController 里添加OrangeView的点击方法
@implementation FindResponderViewController
- (void)viewDidLoad {
[super viewDidLoad];
TAYView *whiteView = [[TAYView alloc] init];
[self.view addSubview:whiteView];
whiteView.backgroundColor = [UIColor whiteColor];
whiteView.frame = self.view.bounds;
BlueView *blueView = [[BlueView alloc] init];
[whiteView addSubview:blueView];
blueView.backgroundColor = [UIColor blueColor];
blueView.frame = CGRectMake(50, 300, 320, 300);
RedView *redView = [[RedView alloc] init];
[whiteView addSubview:redView];
redView.backgroundColor = [UIColor redColor];
redView.frame = CGRectMake(50, 150, 320, 100);
OrangeView *orangeView = [[OrangeView alloc] init];
[blueView addSubview:orangeView];
orangeView.backgroundColor = [UIColor orangeColor];
orangeView.frame = CGRectMake(20, 30, 280, 100);
UITapGestureRecognizer *tapGesturRecognizer=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(click)];
[orangeView addGestureRecognizer:tapGesturRecognizer];
self.yellowView = [[YellowView alloc] init];
[blueView addSubview:_yellowView];
_yellowView.backgroundColor = [UIColor yellowColor];
_yellowView.frame = CGRectMake(20, 160, 280, 100);
}
- (void)click {
NSLog(@"点击了orangeView");
}
打印情况:
可以看到touchesBegan顺着nextResponder链条调用了,但是OrangeView处理了事件,去执行了相关的事件处理方法,而touchesEnded并没有得到调用
OrangeView不处理点击事件
也就是将上面的点击方法注释掉,看看打印情况:
可以看到先是由UIWindow通过hitTest返回所找到的最合适的响应者OrangeView, 接着执行了OrangeView的touchesBegan,然后是通过nextResponder依次是BlueView、TAYView、UIView、FindResponderViewController、UIWindow,可以看到完全是按照nextResponder链条的调用顺序,touchesEnded也是同样的顺序
总结
- nextResponder规则为:如果有父视图则nextResponder指向父视图;如果是根视图则指向控制器;最后指向AppDelegate
- 找到最适合的响应视图后事件会从此视图开始沿着响应链nextResponder传递,直到找到处理事件的视图,如果没有处理的事件会被丢弃
- touchesBegan和touchesEnd调用顺序都是按照nextResponder链条的顺序
无法响应的情况
- Alpha < 0.01
- 子视图超出父视图的情况
- userInteractionEnabled = NO
- hidden = YES
测试代码:
@implementation UnResponderViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
AlphaView *alphaView = [[AlphaView alloc] init];
[self.view addSubview:alphaView];
alphaView.backgroundColor = [UIColor whiteColor];
alphaView.alpha = 0;
alphaView.frame = CGRectMake(50, 50, 100, 50);
FatherView *fatherView = [[FatherView alloc] init];
[self.view addSubview:fatherView];
fatherView.backgroundColor = [UIColor grayColor];
fatherView.frame = CGRectMake(150, 200, 100, 50);
OverView *overView = [[OverView alloc] init];
[fatherView addSubview:overView];
overView.backgroundColor = [UIColor greenColor];
overView.frame = CGRectMake(150, 70, 100, 50);
UserInteractionEnabledView *userInteractionEnabledView = [[UserInteractionEnabledView alloc] init];
[self.view addSubview:userInteractionEnabledView];
userInteractionEnabledView.backgroundColor = [UIColor purpleColor];
userInteractionEnabledView.frame = CGRectMake(150, 300, 100, 50);
userInteractionEnabledView.userInteractionEnabled = NO;
HiddenView *hiddenView = [[HiddenView alloc] init];
[self.view addSubview:hiddenView];
hiddenView.backgroundColor = [UIColor orangeColor];
hiddenView.frame = CGRectMake(150, 400, 100, 50);
hiddenView.hidden = YES;
}
点击后显示如下,大家可以下去自己试试:
参考文献
GitHub
本文中的demo已上传GitHub,可以下载使用:GitHub地址