前言
就iOS而言,app与用户间的交互一般通过UIResponder中的touch类方法,UIControl中的target action方法以及UIGestureRecognizer中的手势来完成。那么三者间的区别和联系究竟是什么?
touch类方法与target action比较
相信大家都知道,UIControl继承自UIView,而UIView继承自UIResponder,即UIControl<--UIView<--UIResponder。所以,先分析touch类方法与target action间的关系。
自定义TControl类,并实现touchesBegan方法
@interface TControl : UIControl
@end
@implementation TControl
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"[%@], %s", NSStringFromClass([self class]), __func__);
[super touchesBegan:touches withEvent:event];
}
@end
在VC中这样写
- (void)viewDidLoad {
[super viewDidLoad];
TControl *control = [[TControl alloc] initWithFrame:self.view.bounds];
[control addTarget:self action:@selector(controlAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:control];
}
- (void)controlAction:(TControl *)control {
NSLog(@"[%@], %s", NSStringFromClass([self class]), __func__);
}
启动后点击屏幕发现,先响应touchesBegan后响应controlAction。在controlAction中打上断点,再次点击后可见调用栈如下:
可见,target action的响应依赖于UIControl中实现的touchesEnded,而touchesEnded的响应需要依赖于touchesBegan,可猜测,如果将TControl类touchesBegan中的super调用方法去掉,controlAction无法响应,即:
@implementation TControl
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"[%@], %s", NSStringFromClass([self class]), __func__);
}
@end
重复以上步骤可发现,controlAction确实不会响应。
结论1:touch类方法优先于target action响应,target action的响应依赖于touch类方法,因此也可通过touch类方法实现阻断target action的响应。
target action与手势比较
添加手势需要调用UIView中的addGestureRecognizer方法,而UIControl继承自UIView(UIControl<--UIView),因此这两者作比较。
如果代码这样写:
@interface TControl : UIControl
@end
@implementation TControl
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"[%@], %s", NSStringFromClass([self class]), __func__);
[super touchesBegan:touches withEvent:event];
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
TControl *control = [[TControl alloc] initWithFrame:self.view.bounds];
[control addTarget:self action:@selector(controlAction:) forControlEvents:UIControlEventTouchUpInside];
// [control addTarget:self action:@selector(controlAction:) forControlEvents:UIControlEventTouchDown];
[self.view addSubview:control];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
[control addGestureRecognizer:tap];
}
- (void)controlAction:(TControl *)control {
NSLog(@"[%@], %s", NSStringFromClass([self class]), __func__);
}
- (void)tapAction:(UITapGestureRecognizer *)tap {
NSLog(@"[%@], %s", NSStringFromClass([self class]), __func__);
}
@end
会发现,无论TControl类的touchesBegan中是否调用super,tapAction始终响应,controlAction始终不响应,并且响应顺序为先touch后gesture。同样在tapAction中打上断点,再次点击后调用栈如下:
可以看到,并没有任何的touch类方法被调用,这也说明了为什么TControl中的touchesBegan即使没调用super,手势依然可以响应。
同时,在UIGestureRecognizer文档中可以找到如下描述:
A gesture recognizer doesn’t participate in the view’s responder chain.
即,手势识别器不参与响应链传递,由此可得:touch类方法优先gesture响应,但gesture响应不依赖于touch类方法。
那么是否说明gesture的响应优先于target action?毕竟在TControl类的touchesBegan中调用了super,但是target action并没有响应。
继续改造代码:
@implementation TControl
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"[%@], %s", NSStringFromClass([self class]), __func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"[%@], %s", NSStringFromClass([self class]), __func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"[%@], %s", NSStringFromClass([self class]), __func__);
[super touchesCancelled:touches withEvent:event];
}
@end
运行后点击屏幕发现,touchesCancelled被调用从而导致touchesEnded不会响应,上文分析过当UIControl的UIControlEvents为UIControlEventTouchUpInside
时,响应action需要调用UIControl的touchesEnded方法才行。此时touchesEnded不响应,所以target action不响应。
同理,在touchesCancelled打上断点再次点击屏幕后查看调用栈,可以发现的确是经gesture传递后调用的touchesCancelled方法。
在UIGestureRecognizer.h中搜索cancel关键字,会发现cancelsTouchesInView
这个BOOL属性。属性默认值为YES,当手势被识别后会调用手势附加view的touchesCancelled方法,此时可能会导致部分事件无法响应。当属性设置为NO时,响应链正常传递。
tap.cancelsTouchesInView = NO;
当加上这句代码时,可以发现tapAction和controlAction都是可以响应的。
顺便说一下UIGestureRecognizer中另外两个和touch相关的属相delaysTouchesBegan
、delaysTouchesEnded
。这两个属性也都是BOOL值,其中delaysTouchesBegan默认值为NO,delaysTouchesEnded默认值为YES。
上文通过分析得出过一个结论:touch类方法优先gesture响应,但gesture响应不依赖于touch类方法。
其实并不完全正确,这个结论的前提是delaysTouchesBegan == NO
,如果设置为YES,touch类的方法并不会调用。
tap.delaysTouchesBegan = YES;
结论2:当gesture. delaysTouchesBegan == NO 时,touch类方法优先gesture响应。当gesture. delaysTouchesBegan == YES 时,只响应gesture方法。同时,gesture响应不依赖于touch类方法。
当gesture. delaysTouchesBegan == YES
时,在手势识别的过程中不会响应touch类方法。在手势识别失败后,延迟调用当前view的touchesBegan方法(值为NO时,识别失败不延时调用,识别成功不调用)。
同样,当gesture.delaysTouchesEnded == YES
时,在手势识别的过程中不会响应touch类方法。在手势识别失败后,延迟调用当前view的touchesEnded方法(值为NO时,识别失败不延时调用,识别成功不调用)。
易错点
本文采用的是UIControl的UIControlEventTouchUpInside事件与UITapGestureRecognizer做对比,细心的看官可以发现,code中有一句注释掉的代码
// [control addTarget:self action:@selector(controlAction:) forControlEvents:UIControlEventTouchDown];
这里写的是UIControl的UIControlEventTouchDown事件,这个事件只要control的touchesBegan可以响应就会调用,不需要touchesEnded。所以具体事件具体分析,理解原理后万变不离其宗。