iOS Touch事件处理和手势的一些细节思考

8 阅读11分钟

写在前面

对于一个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。每次测试都是双击蓝色区域。

截屏2026-03-16 14.29.49.png

思考

gesture和touches的关系

在hitTest过程,会把响应链上view的手势添加到touch(UITouch)上。断点在 touchesBegin 方法中如下图所示:UITouch的属性gestureRecognizers,可以看到有Doubel/SingleTapGesture。

截屏2026-03-16 14.47.10.png

事件处理流程

我们知道,当有事件(这里仅讨论touch事件)发生。如果有手势,则会考虑手势如何处理这个事件,然后决定是否发送给first responser,当有多个手势,需要考虑手势之间的冲突处理。

未命名绘图.drawio.png

touchesBegin 消息转发过程

当在响应链上转发touchesBegin消息过程,会建立对应的forwardingRecord,如下图所示: 当点击customView2,消息touchesBegin发送给firstResponser(也就是customView2),消息再沿着响应链路转发到customView1。此时该touch的forwardingRecord记录了一次转发过程。如下图所示: 可以看到,一条record记录了fromResponser(customView2)和responser(customView1)。 截屏2026-03-16 15.14.14.png 当整个touchesBegin在消息响应链路上完整转发后,一个完整forwardingRecord就会生成,如下图所示:断点在 customView2 的touchesBegin调用完super方法之后,表示消息已经完整的转发了。

截屏2026-03-16 15.07.38.png

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传递 保持上面的代码,打印结果如下:

image.png

  • 中断touchesBegin传递 注释掉customView1的touchesBegin方法中的super调用,其它保持不变。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan1");
   //[super touchesBegan:touches withEvent:event];、
}

打印结果如下:

截屏2026-03-16 15.21.00.png 可以看出touchesBegin没有被转发到customView1的下一个responser,对于touchesEnded消息也就没有被完整的转发,对应的原理应该是根据touch的forwardingRecoed决定转发路径。打印出customView2中的touchEnded方法中的touch参数的属性forwardingRecord如下图所示:

只有一条转发记录,所以转发到customView1中就结束了,并且继续向下一个responser转发。不过这样符合常识,当responser没有收到touchesBegin消息,何必再收到touchesEnded消息。这里也从侧面证明,touchesEnded/Cancelled的消息转发依赖于touchesBegin转发过程生成的forwardingRecord。

image.png

其实在官方文档我们能看到这么一句话:

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

打印结果:

image.png 分析:

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

打印结果:

image.png

分析:

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

打印结果:

image.png

分析: 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

打印结果:

image.png

分析: 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

打印结果:

image.png

分析: 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

打印结果:

image.png 分析: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

打印结果:

image.png

分析:

分析: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

打印结果:

image.png

分析: 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

总结

  1. 在hitTest过程,会沿着传递链添加responser上的手势
  2. 发送给firstResponser的touchesBegin消息,沿着响应链路会生成UItouch的forwardingRecord。
  3. 当一个UITouch没有发送touchBegin给firstResponser,则touchCancelled也不会发送给firstResponser
  4. 是否给firstResponser发送touch相关消息,取决于gesture的设置和状态。
  5. 尽信AI不如无AI。