写在前面
对于一个iOS开发者,手势是不可避免处理。突发奇想了一些特殊的场景以及手势的参数处理,想想系统方法的调用机制会是什么呢(下面会有具体的代码),于是在 测试->总结->推翻之前的结论->测试->总结->推翻之前的结论 这样的循环过程,借助豆包和DeepSeek摸索出了一些经验,当然不能说我最后得出的结论一定正确,在我测试代码以及打印出的Log起码能说服自己,因为系统的实现是一个黑盒子,我们只能基于打印出来的日志,来验证自己的想法是否正确。
最后需要提到的一点是,这篇文章不会详细介绍整个iOS事件处理的过程以及完整的手势介绍,阅读这篇文章需要你有一些前置知识,可以参考文章iOS事件及手势处理流程和不用但一定要懂 ---- iOS 之 响应链、传递链 与 手势识别。
测试代码
添加手势
假设我们有一个customView1和customView2,其中customView2是customView1的subView,其中customView1上添加了一个singleTap的单击手势,customView2上添加了一个doubleTap的双击手势。
// singleTap的类型是TestSingleTapGesture,仅仅是继承了UITapGestureRecognizer,
// 重写了touches一系列touches(UIGestureRecognizer)方法打印Log。
self.singleTap = [[TestSingleTapGesture alloc] initWithTarget:self action:@selector(singleTap:)];
self.singleTap.delegate = self;
self.singleTap.numberOfTapsRequired = 1;
self.singleTap.delaysTouchesBegan = NO;
self.singleTap.delaysTouchesEnded = YES;
self.singleTap.cancelsTouchesInView = YES;
// customView1 的类型是自定义的CustomView1,仅仅是继承了UIView,重写了touches(UIResponder)一系列方法打印Log。
[self.customView1 addGestureRecognizer:self.singleTap];
// doubleTap的类型是TestDoubleTapGesture,仅仅是继承了UITapGestureRecognizer,
// 重写了touches一系列touches(UIResponder)方法打印Log。
self.doubleTap = [[TestDoubleTapGesture alloc] initWithTarget:self action:@selector(doubleTap:)];
self.doubleTap.delegate = self;
self.doubleTap.numberOfTapsRequired = 2;
self.doubleTap.delaysTouchesBegan = NO;
self.doubleTap.delaysTouchesEnded = YES;
self.doubleTap.cancelsTouchesInView = YES;
// customView2 的类型是自定义的CustomView2,仅仅是继承了UIView,重写了touches(UIGestureRecognizer)一系列方法打印Log。
[self.customView2 addGestureRecognizer:self.doubleTap];
// 单击手势识别成功
- (void)singleTap:(UIGestureRecognizer*)tap {
NSLog(@"touches single tap recognition");
}
// 双击手势识别成功
- (void)doubleTap:(UIGestureRecognizer*)tap {
NSLog(@"touches double tap recognition");
}
CustomView
CustomView1
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan1");
[super touchesBegan:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesCancelled1");
[super touchesCancelled:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesEnded1");
[super touchesEnded:touches withEvent:event];
}
CustomView2
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan2");
[super touchesBegan:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesCancelled2");
[super touchesCancelled:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesEnded2");
[super touchesEnded:touches withEvent:event];
}
TestDoubleTapGesture
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touches gesture begin2");
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touches gesture Ended2");
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touches gesture cancelled2");
[super touchesCancelled:touches withEvent:event];
}
界面展示 蓝色是customView2,红色customView1。每次测试都是双击蓝色区域。
思考
gesture和touches的关系
在hitTest过程,会把响应链上view的手势添加到touch(UITouch)上。断点在 touchesBegin 方法中如下图所示:UITouch的属性gestureRecognizers,可以看到有Doubel/SingleTapGesture。
事件处理流程
我们知道,当有事件(这里仅讨论touch事件)发生。如果有手势,则会考虑手势如何处理这个事件,然后决定是否发送给first responser,当有多个手势,需要考虑手势之间的冲突处理。
touchesBegin 消息转发过程
当在响应链上转发touchesBegin消息过程,会建立对应的forwardingRecord,如下图所示:
当点击customView2,消息touchesBegin发送给firstResponser(也就是customView2),消息再沿着响应链路转发到customView1。此时该touch的forwardingRecord记录了一次转发过程。如下图所示:
可以看到,一条record记录了fromResponser(customView2)和responser(customView1)。
当整个touchesBegin在消息响应链路上完整转发后,一个完整forwardingRecord就会生成,如下图所示:断点在 customView2 的touchesBegin调用完super方法之后,表示消息已经完整的转发了。
touchesBegin消息转发链路会不会影响touchEnded和touchCancel消息的转发?
为了测试这个,我们在customView1的view controller重写如下方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touches view controller begin");
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touches view controller end");
[super touchesEnded:touches withEvent:event];
}
- 不中断touchesBegin传递 保持上面的代码,打印结果如下:
- 中断touchesBegin传递 注释掉customView1的touchesBegin方法中的super调用,其它保持不变。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan1");
//[super touchesBegan:touches withEvent:event];、
}
打印结果如下:
可以看出touchesBegin没有被转发到customView1的下一个responser,对于touchesEnded消息也就没有被完整的转发,对应的原理应该是根据touch的forwardingRecoed决定转发路径。打印出customView2中的touchEnded方法中的touch参数的属性forwardingRecord如下图所示:
只有一条转发记录,所以转发到customView1中就结束了,并且继续向下一个responser转发。不过这样符合常识,当responser没有收到touchesBegin消息,何必再收到touchesEnded消息。这里也从侧面证明,touchesEnded/Cancelled的消息转发依赖于touchesBegin转发过程生成的forwardingRecord。
其实在官方文档我们能看到这么一句话:
Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each touch it is handling (those touches it received in touchesBegan:withEvent:).
翻译过来就是 只有收到touchesBegin消息才有可能收到touchesEnd或者touchesCancelled消息
// 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.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches API_AVAILABLE(ios(9.1));
delayTouchesBegin,delayTouchesEnded和cancelsTouchesInView
手势的这些属性设置,影响touch相关的消息发送给firstResponser。在这里保持singleTap的设置保持默认值。每次都是双击customView2(添加了双击手势),customView1(添加了单击手势)。 touch1Begin表示手指第一次按下,touch1Ended表示手指第一次移开。其中这是一个touch(内存中一个对象表示),只是状态不同。
touch2Begin表示手指第二次按下,touch2Ended表示手指第二次移开。其中这是一个touch(内存中一个对象表示),只是状态不同。但是与上面的touch1是两个不同的touch。
当手势touch事件,基于前面介绍的,在hitTest会收集响应链路上的gesture,手势识别管理中心(姑且叫这个名字吧)会根据touch上的手势设置以及状态来决定是否给firstResponser发送对应的消息。
self.singleTap.delaysTouchesBegan = NO;
self.singleTap.delaysTouchesEnded = YES;
self.singleTap.cancelsTouchesInView = YES;
更改doubleTap的属性设置:
- delaysTouchesBegan = YES
- delaysTouchesEnded = YES
- cancelsTouchesInView = YES
打印结果:
分析:
touch1Begin到达,因为doubleTap的delaysTouchesBegan为YES,而singleTap的delaysTouchesBegan为NO,手势识别中心决定不发送touchBegin消息。
touch1End到达,singleTap识别成功,由于singleTap的cancelsTouchesInView为YES,按照应该发送touchesCancel消息给firstResponser,但是由于前面没有发送touchesBegion。所以不发送touchesCancel。打印 touches single tap recognition。
touch2Begin到达,基于和上面touch1Begin一样的理由,不发送touchesBegin。
touch2Ended到达,doubleTap识别成功,基于和上面touch1Ended一样的理由,不发送touchesCancel。打印 touches double tap recognition。
更改doubleTap的属性设置为:
- delaysTouchesBegan = NO
- delaysTouchesEnded = YES
- cancelsTouchesInView = YES
打印结果:
分析:
touch1Begin到达,因为doubleTap的delaysTouchesBegan为NO,而singleTap的delaysTouchesBegan也为NO,手势识别中心决定发送touchBegin消息, 沿着响应链路打印: touchesBegan2 touchesBegan1
touch1End到达,singleTap识别成功。打印 touches single tap recognition。
由于singleTap的cancelsTouchesInView为YES,应该发送touchesCancel消息给firstResponser,前面发送过touchesBegin,手势识别中心决定发送touchCancelled消息。 沿着响应链路打印: touchesCancelled2 touchesCancelled1
touch2Begin到达,double tap的delayTouchEnded为YES,begin1Ended未发送,认为这是一个触摸序列,则不发送touchesBegin。
touch2Ended到达,doubleTap识别成功,打印 touches double tap recognition。 cancelsTouchesInView为YES,按照原理应该发送touchesCancelled,但是由于没有发送touchesBegin,则不发送touchesCancelled。
更改doubleTap的属性设置为:
- delaysTouchesBegan = YES
- delaysTouchesEnded = NO
- cancelsTouchesInView = YES
打印结果:
分析: touch1Begin到达,因为doubleTap的delaysTouchesBegan为YES,而singleTap的delaysTouchesBegan为NO,手势识别中心决定不发送touchBegin消息。
touch1End到达,singleTap识别成功,由于singleTap的cancelsTouchesInView为YES,按照应该发送touchesCancel消息给firstResponser,但是由于前面没有发送touchesBegion。所以不发送touchesCancel。打印 touches single tap recognition。
touch2Begin到达,基于和上面touch1Begin一样的理由,不发送touchesBegin。
touch2Ended到达,doubleTap识别成功,打印 touches double tap recognition。 基于和上面touch1Ended一样的理由,不发送touchesCancel。
更改doubleTap的属性设置为:
- delaysTouchesBegan = YES
- delaysTouchesEnded = NO
- cancelsTouchesInView = NO
打印结果:
分析: touch1Begin到达,因为doubleTap的delaysTouchesBegan为YES,而singleTap的delaysTouchesBegan为NO,手势识别中心决定不发送touchBegin消息。
touch1End到达,singleTap识别成功,打印 touches single tap recognition。
由于singleTap的cancelsTouchesInView为YES,按照应该发送touchesCancel消息给firstResponser,但是由于前面没有发送touchesBegion。所以不发送touchesCancel。
touch2Begin到达,基于和上面touch1Begin一样的理由,不发送touchesBegin。
touch2Ended到达,doubleTap识别成功,打印 touches double tap recognition。
cancelsTouchesInView为NO,则应该发送touchesEnded,由于touch2Begin到达没有发送touchesBegin消息,touch上没有生成forwardingRecord,所以仅仅转发到customView2上,就没有继续转发。打印touchesEnd2。
更改doubleTap的属性设置为:
- delaysTouchesBegan = NO
- delaysTouchesEnded = NO
- cancelsTouchesInView = YES
打印结果:
分析: touch1Begin到达,因为doubleTap的delaysTouchesBegan为NO,而singleTap的delaysTouchesBegan为NO,手势识别中心决定发送touchBegin消息, 沿着响应链路打印: touchesBegan2 touchesBegan1
touch1End到达,singleTap识别成功,打印 touches single tap recognition。
由于singleTap的cancelsTouchesInView为YES,应该发送touchesCancel消息给firstResponser,前面发送过touchesBegin,手势识别中心决定发送touchCancelled消息。 沿着响应链路打印: touchesCancelled2 touchesCancelled1
touch2Begin到达,基于和上面touch1Begin一样的理由,手势识别中心决定发送touchBegin消息, 沿着响应链路打印: touchesBegan2 touchesBegan1
touch2Ended到达,doubleTap识别成功,打印 touches double tap recognition。
cancelsTouchesInView为YES,应该发送touchesCancel消息给firstResponser,前面发送过touchesBegin,手势识别中心决定发送touchCancelled消息。 沿着响应链路打印: touchesCancelled2 touchesCancelled1
更改doubleTap的属性设置为:
- delaysTouchesBegan = NO
- delaysTouchesEnded = YES
- cancelsTouchesInView = NO
打印结果:
分析:touch1Begin到达,因为doubleTap的delaysTouchesBegan为NO,而singleTap的delaysTouchesBegan为NO,手势识别中心决定发送touchBegin消息,
沿着响应链路打印:
touchesBegan2
touchesBegan1
touch1End到达,singleTap识别成功,打印 touches single tap recognition。
由于singleTap的cancelsTouchesInView为YES,应该发送touchesCancel消息给firstResponser,前面发送过touchesBegin,手势识别中心决定发送touchCancelled消息。 沿着响应链路打印: touchesCancelled2 touchesCancelled1
touch2Begin到达,double tap的delayTouchEnded为YES,begin1Ended未发送,认为这是一个触摸序列,则不发送touchesBegin。
touch2Ended到达,doubleTap识别成功,打印 touches double tap recognition。
cancelsTouchesInView为NO,则应该发送touchesEnded,由于touch2Begin到达没有发送touchesBegin消息,touch上没有生成forwardingRecord,所以仅仅转发到customView2上,就没有继续转发。打印touchesEnd2。
更改doubleTap的属性设置为:
- delaysTouchesBegan = NO
- delaysTouchesEnded = NO
- cancelsTouchesInView = NO
打印结果:
分析:
分析:touch1Begin到达,因为doubleTap的delaysTouchesBegan为NO,而singleTap的delaysTouchesBegan为NO,手势识别中心决定发送touchBegin消息, 沿着响应链路打印: touchesBegan2 touchesBegan1
touch1End到达,singleTap识别成功,打印 touches single tap recognition。
由于singleTap的cancelsTouchesInView为YES,应该发送touchesCancel消息给firstResponser,前面发送过touchesBegin,手势识别中心决定发送touchCancelled消息。 沿着响应链路打印: touchesCancelled2 touchesCancelled1
touch2Begin到达,double tap的delayTouchEnded为NO,begin1Ended没有被Delay丢弃。touch1Begin作为一个新的开始触摸事件,手势识别中心决定发送touchBegin消息。 沿着响应链路打印: touchesBegan2 touchesBegan1
touch2Ended到达,doubleTap识别成功,打印 touches double tap recognition。
cancelsTouchesInView为NO,则应该发送touchesEnded,由于touch2Begin到达发送touchesBegin消息,touch生成了forwardingRecord。沿着响应链路打印: touchesEnd2 touchesEnd1
更改doubleTap的属性设置为:
- delaysTouchesBegan = YES
- delaysTouchesEnded = YES
- cancelsTouchesInView = NO
打印结果:
分析: touch1Begin到达,因为doubleTap的delaysTouchesBegan为YES,而singleTap的delaysTouchesBegan为NO,手势识别中心决定不发送touchBegin消息。
touch1End到达,singleTap识别成功,打印 touches single tap recognition。
由于singleTap的cancelsTouchesInView为YES,按照应该发送touchesCancel消息给firstResponser,但是由于前面没有发送touchesBegion。所以不发送touchesCancel。
touch2Begin到达,double tap的delayTouchEnded为YES,begin1Ended未发送,认为这是一个触摸序列,则不发送touchesBegin。
touch2Ended到达,doubleTap识别成功,打印 touches double tap recognition。
cancelsTouchesInView为NO,则应该发送touchesEnded,由于touch2Begin到达没有发送touchesBegin消息,touch上没有生成forwardingRecord,所以仅仅转发到customView2上,就没有继续转发。打印touchesEnd2。
总结
- 在hitTest过程,会沿着传递链添加responser上的手势
- 发送给firstResponser的touchesBegin消息,沿着响应链路会生成UItouch的forwardingRecord。
- 当一个UITouch没有发送touchBegin给firstResponser,则touchCancelled也不会发送给firstResponser
- 是否给firstResponser发送touch相关消息,取决于gesture的设置和状态。
- 尽信AI不如无AI。