「OC」剪不断,理还乱——UIResponder、UIGestureRecognizer、UIControl的响应优先级探究
文章目录
前言
上一篇我们记录了编译器是如何找到最佳响应者以及响应者链的构成「OC」初识iOS事件处理流程,那么今天我们就来探究,UIResponder、UIGestureRecognizer、UIControl在处理事件时的优先级,以及相关的细节。让我们开始UIResponder、UIGestureRecognizer、UIControl的爱恨情仇吧
介绍
先简单介绍一下本文的三个主角
UIResponder
UIResponder是iOS中处理事件的核心类,它是许多重要UI类的父类,包括UIApplication、UIView和UIViewController。
主要特点:
- 定义了响应和处理事件的接口
- 是构建响应者链的基础
- 可以处理触摸事件、运动事件、远程控制事件等
UIGestureRecognizer
UIGestureRecognizer是一个抽象基类,用于检测特定的触摸手势。它的继承了UIView,而UIView又继承于UIResponder,所以在某些时候我们可以将两者等同看待,在细节方面有相似之处
主要特点:
- 简化了复杂手势的识别过程
- 可以添加到任何UIView上
- 有多个具体子类,如UIPanGestureRecognizer, UITapGestureRecognizer等
补充:其实手势接受事件也是通过与其他两者类似的方式,即触摸开始——触摸移动——触摸取消——触摸结束,他们被写在了UIGestureRecognizerSubclass.h之中,我们下文探究触摸优先级时,打印日志需要在UITapGestureRecognizer的子类之中引入
import <UIKit/UIGestureRecognizerSubclass.h>
手势分为离散型手势(discrete gestures)和持续型手势(continuous gesture)。系统提供的离散型手势包括点按手势(UITapGestureRecognizer)和轻扫手势(UISwipeGestureRecognizer
),其余均为持续型手势。
在手势之中,有三个属性比较重要
@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;
cancelsTouchesInView:默认值为YES,表示当手势识别器成功识别了手势之后,会通知Application取消响应链对事件的响应,并不再传递事件给hit-test view。若设置成NO,表示手势识别成功后不取消响应链对事件的响应,事件依旧会传递给hit-test view。
delaysTouchesBegan:默认为NO。默认情况下手势识别器在识别手势期间,当触摸状态发生改变时,Application都会将事件传递给手势识别器和hit-tested view;若设置成YES,则表示手势识别器在识别手势期间,截断事件,即不会将事件发送给hit-tested view。
delaysTouchesEnded:默认为YES。当手势识别失败时,若此时触摸已经结束,会延迟一小段时间(0.15s)再调用响应者的 touchesEnded:withEvent:
;若设置成NO,则在手势识别失败时会立即通知Application发送状态为end的touch事件给hit-tested view以调用 touchesEnded:withEvent:
结束事件响应。
UIControl
UIControl是一个抽象基类,为特定的用户界面元素(如按钮、开关、滑块等)提供了基础结构。
主要特点:
- 继承自UIView
- 定义了一组标准的控制事件(如触摸按下、抬起、拖动等)
- 允许为不同事件添加多个目标动作
target-action
- target:处理交互事件的对象
- action:处理交互事件的方式
UIControl作为能够响应事件的控件,必然也需要待事件交互符合条件时才去响应,因此也会跟踪事件发生的过程。不同于UIResponder以及UIGestureRecognizer通过 touches
系列方法跟踪,UIControl有其独特的跟踪方法:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;
大致方法其实与我们先前接触的UIResponder的那4个方法相同,毕竟UIControl本身也是UIResponder的子类,但是这个UIControl之中的tracking
系列的方法是在touch
系列之中进行实现的。
具体的target-action具体流程,我按着网上给出的内容叙述一遍:
UIControl控件通过 addTarget:action:forControlEvents:
添加事件处理的target和action,当事件发生时,会调用 sendAction:to:forEvent:
将target、action以及event对象发送给全局应用,Application对象再通过 sendAction:to:from:forEvent:
向target发送action。
另外,若不指定target,即 addTarget:action:forControlEvents:
时target传空,那么当事件发生时,Application会在响应链上从上往下寻找能响应action的对象。
正文
接下来我们来探究一下,事件产生的先后优先顺序
我们先写一个简单的demo进行测试
#import "ViewController.h"
@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray *dataSource;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"手势冲突测试";
self.view.backgroundColor = [UIColor whiteColor];
// 初始化数据源
self.dataSource = @[@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8"];
// 创建并设置UITableView
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.backgroundColor = [UIColor orangeColor];
self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
[self.view addSubview:self.tableView];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionTapView)];
[self.view addGestureRecognizer:tap];
UIButton *bottomButton = [UIButton buttonWithType:UIButtonTypeCustom];
[bottomButton setTitle:@"点我无压力" forState:UIControlStateNormal];
[bottomButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
bottomButton.backgroundColor = [UIColor cyanColor];
bottomButton.frame = CGRectMake(20, CGRectGetMaxY(self.view.bounds) - 60, CGRectGetWidth(self.view.bounds) - 40, 40);
[bottomButton addTarget:self action:@selector(buttonTap) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:bottomButton];
}
-(void)buttonTap {
NSLog(@"button clicked!");
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataSource.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 55;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
cell.backgroundColor = [UIColor orangeColor];
cell.textLabel.textColor = [UIColor whiteColor];
}
cell.textLabel.text = self.dataSource[indexPath.row];
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(@"cell selected!");
}
- (void)actionTapView{
NSLog(@"backview taped");
}
@end
我们进行点击进行尝试
可以发现当我短按cell的时候,触发的是手势事件,只有长按cell才能够触发点击cell的方法,这到底是为什么啊?
由于这三兄弟,都实现了类似的方法,所以我在他们的子类之中,增加的打印日志的行为,以探究这三者的运行关系
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
具体打印日志的方式如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesCancelled:touches withEvent:event];
}
UIGestureRecognizer和UIResponder
关于UIGestureRecognizer和UIResponder的问题,其实上面UIGestureRecognizer的属性已经给我们透露一些相关的内容了,那我们就先从我们最有疑问的开始探究吧。
短按
在短按的时候我们可以看到,其实在我们短按的时候,手势识别先开始,而tableView的触摸在随后也开始了,之所以我们的无法触发选择cell的方法是因为,cell的触摸在手势触发之后被Application给取消了。
引用官方文档的翻译:Window在将事件传递给hit-tested view之前,会先将事件传递给相关的手势识别器并由手势识别器优先识别。若手势识别器成功识别了事件,就会取消hit-tested view对事件的响应;若手势识别器没能识别事件,hit-tested view才完全接手事件的响应权。
因此通过这个点,我们可以总结出:手势识别器比UIResponder具有更高的事件响应优先级!!
快速按下
有的读者可能就有疑惑了,咋一听,如果短按的道理是上面解释的那样,那么按理来说快速按下的响应过程和短按应该是相同的才对。
我们知道UITableView是UIScrollView的子类,而UIScrollView因为要滚动,所以对事件做了特殊的处理: 当UIScrollView接收到事件之后,会暂时劫持当前的事件,会产生一个UIScrollViewDelayedTouchesBeganGestureRecognizer
的手势识别器识别是否进行滚动操作,由于手势操作在应用之中可以进行同时识别,所以当UIScrollView这个机制劫持了触摸事件,和TableViewtouchesBegan:
的调用时间相差大概为0.15s,由于UIScrollViewDelayedTouchesBeganGestureRecognizer
拦截了touchesBegan:
发送,当我们点击时间过短的话,没过延迟时间触摸就结束了,因此就会出现以下情况。
长按
由于我们手势劫持判断是有时效的,当时效结束之后,hit-tested view才完全接手事件,停止向手势识别器发送事件,仅向hit-test view发送事件。
补充
刚刚我们讲的是离散型的手势,接下来我们补充一下持续性手势,仍是相同的内容但是把手势改为持续性的手势,
我们可以看到即便滑动手势识别器识别了手势,Application也会依旧发送事件给tableView。
另外,在滑动的过程中,若手势识别器未能识别手势,则事件在触摸滑动过程中会一直传递给hit-tested view,直到触摸结束。
乱上加乱
如果让本就纠缠不清,复杂无比的UIGestureRecognizer和UIResponder,再有第三者UIControl插足呢?
真是头大,看的我都快看不懂字了。
根据官方文档,总结内容如下:UIControl会阻止父视图上的手势识别器行为,也就是UIControl处理事件的优先级比UIGestureRecognizer高,但前提是相比于父视图上的手势识别器。
我们设置一下情景
在view上添加点击手势识别器
点击button后,事件先传递给手势识别器,再传递给作为hit-tested view存在的button(UIControl本身也是UIResponder),由于button阻止了父视图BlueView中的手势识别器的识别,导致手势识别器识别失败,button完全接手了事件的响应权,事件最终由button响应;
在button上添加手势识别器
点击button后,事件先传递给手势识别器,再传递给作为hit-tested view存在的button(UIControl本身也是UIResponder),button未阻止其本身绑定的手势识别器的识别,因此手势识别器先识别手势并识别成功,而后通知Application取消响应链对事件的响应,因为 touchesCancelled 被调用,同时 cancelTrackingWithEvent 跟着调用,因此button的target-action得不到执行。
所以我们可以得出结论:
UIControl比其父视图上的手势识别器具有更高的事件响应优先级。
总结
最后我们可以笼统的得出一个结论:UIRespnder、UIGestureRecognizer、UIControl,笼统地讲,事件响应优先级依次递增。