目录
一、基本概念
1.1、UITouch
1.2、UIEvent
1.3、UIResponder
二、查找第一响应者
三、响应者链
一、基本概念
在研究触摸事件之间先看下一些重要的类。
1.1、UITouch
An object representing the location, size, movement, and force of a touch occurring on the screen.
表示发生在屏幕上的可以表示触摸的位置,大小,移动和力度的一个对象
通俗来讲就是一个手指触摸会生成一个UITouch对象,多个手指触摸会生成多个UITouch对象。
UITouch的部分声明如下:
@interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval timestamp; //触摸的时间
@property(nonatomic,readonly) UITouchPhase phase; //触摸的阶段
@property(nonatomic,readonly) NSUInteger tapCount; //在一特定的时间内触摸某一个点的次数,可用来判断单击、双击、三击
@property(nonatomic,readonly) UITouchType type; //触摸的类型
@property(nonatomic,readonly) CGFloat force; //触摸力度,平均值是1.0
@property(nullable,nonatomic,readonly,strong) UIWindow *window; //触摸发生时的window
@property(nullable,nonatomic,readonly,strong) UIView *view; //发生触摸时的view,第一响应者
@end
每一个UITouch都会包含一个UITouchType(可以查看UITouch类定义,其内部有一个类型是UITouchType的属性type),其类型定义如下
typedef NS_ENUM(NSInteger, UITouchType) {
UITouchTypeDirect, // 直接触摸屏幕产生的touch
UITouchTypeIndirect, // 非直接触摸屏幕产生的touch
UITouchTypePencil NS_AVAILABLE_IOS(9_1), // 使用触摸笔产生的touch
UITouchTypeStylus NS_AVAILABLE_IOS(9_1) = UITouchTypePencil, // 废弃, 使用触摸笔 选项
} NS_ENUM_AVAILABLE_IOS(9_0);
每一个UITouch都会包含一个UITouchPhase(可以查看UITouch类定义,其内部有一个类型是UITouchPhase的属性phase),其类型定义如下
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
1.2、UIEvent
An object that describes a single user interaction with your app.
描述和app单次交互的对象。
UIEvent的部分声明如下:
@interface UIEvent : NSObject
@property(nonatomic,readonly) UIEventType type; //事件的类型
@property(nonatomic,readonly) UIEventSubtype subtype; //事件的子类型
@property(nonatomic,readonly) NSTimeInterval timestamp; //事件发生的时间
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches; //每个事件中包含的触摸集合
@end
从上可以看出来每一个UIEvent事件可能包含多个UITouch,比如多指触摸。
每一个UIEvent都会包含一个UIEventType(可以查看UIEvent类定义,其内部有一个类型是UIEventType的属性type),其类型定义如下
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches, // 触摸事件,按钮、手势等
UIEventTypeMotion, // 运动事件,摇一摇、指南针等
UIEventTypeRemoteControl, //使用遥控器或耳机线控等产生的事件。如播放、暂停等。
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0), // 3D touch
};
1.3、UIResponder
An abstract interface for responding to and handling events.
响应和处理事件的一个抽象接口。
响应者对象(UIResponder的实例)构成app事件处理的基础。许多关键的对象都是UIResponder的实例,比如UIApplication, UIWindow, UIViewController,以及所有的UIView.当前事件发生的时候,UIKit会自动将其派发到合适的对象来处理,这个对象就叫做第一响应者。
UIResponder中有很多的事件,其中包括处理触摸事件、点按事件、加速事件、远程控制事件。如果想响应事件,那么对应的UIResponder必须重写相应事件的方法,比如如果处理触摸事件,可以重写touchesBegan、touchesMoved、touchesEnded、touchesCancelled方法
UIResponder除了处理事件之外还管理着如何让未处理的事件传递到其它的responder对象。如果一个指定的responder对象未处理事件,那么它会沿着响应者链传递到另一个responder对象。当前下一个responder对象可能是superView或者ViewController。
UIResponder的部分声明如下:
@interface UIResponder : NSObject <UIResponderStandardEditActions>
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO
@property(nonatomic, readonly) BOOL canResignFirstResponder; // default is YES
@property(nonatomic, readonly) BOOL isFirstResponder;
// 处理触摸事件的主要方法,所以我们自定义事件处理时,就需要在这几个方法里面做文章。
- (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 NS_AVAILABLE_IOS(9_1);
//点按事件
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
//加速事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
//远程控制事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
@end
🔥 总结: UITouch、UIEvent、UIResponder的关系
UITouch 是一次“触摸点”,UIEvent 是“一组触摸的集合”,UIResponder 是“处理这些事件的对象”。
二、响应者链(The Responder Chain)
iOS事件是沿着响应者链进行传递的,🔥所谓响应者链即有响应者对象组成的链条
响应者链的目的是🔥在这个链条上找到一个最终可以处理事件的对象
The ultimate goal of these event paths is to find an object that can handle
and respond to an event
下面是官方旧版本事件传递的过程:

上图解释如下:
触摸了
initial view1.第一响应者就是
initial view, 即initial view首先响应touchesBegan:withEvent:方法,如果其没有重写touchesBegan:withEvent:或者重写了但是调用了super方法那么事件会传递给橘黄色的view2.橘黄色的view开始响应
touchesBegan:withEvent:方法,如果其没有重写touchesBegan:withEvent:或者重写了但是调用了super方法那么事件会传递给蓝绿色view3.蓝绿色view响应
touchesBegan:withEvent:方法,如果其没有重写touchesBegan:withEvent:或者重写了但是调用了super方法那么事件会传递给控制器的view4.控制器view响应
touchesBegan:withEvent:方法,如果其没有重写touchesBegan:withEvent:或者重写了但是调用了super方法那么事件会传递给控制器传递给了窗口5.窗口再传递给application
如果app不能处理这个事件,那么这个事件将被废弃
下面是官方新版本事件传递的过程:

解释如下:
- 如果
text field未处理事件,UIKit会将事件传递给text field中parent view--UIView,也即图中的第一个UIView, - 如果第一个
UIView未处理事件,UIKit会将事件传递给第二个UIView - 如果第二个
UIView未处理事件,正常情况下应该将事件传递给UIWindow。但是UIViewController也是UIResponder的子类,所以事件将传递给UIViewController - 如果
UIViewController未处理事件,UIKit会将事件传递给第UIWindow - 如果
UIWindow未处理事件,UIKit会将事件传递给第UIApplication,当然也会传递给是UIResponder的子类但不属于响应者链环节的delegate - 如果app不能处理这个事件,那么这个事件将被废弃
三、查找第一响应者
第一响应者的类型和事件的类型有关系:
| 事件类型 | 第一响应者 |
|---|---|
| Touch events | The view in which the touch occurred. |
| Press events | The object that has focus. |
| Shake-motion events | The object that you (or UIKit) designate. |
| Remote-control events | The object that you (or UIKit) designate. |
| Editing menu messages | The object that you (or UIKit) designate. |
现在我们只关心触摸事件
查找第一响应者的原理如下:UIKit 会使用基于view的hit-testing方法来查找第一响应者。明确地说就是利用触摸发生的位置和视图层级中每个view的bounds进行比较(其实就是调用pointInside:withEvent,如果返回YES表示在这个范围内,NO则相反)。也就是说通过视图中的hitTest:withEvent方法遍历视图层级,直至找到包含触摸点的子view,那个view就是第一响应者。
- 备注:如果触摸点在一个view 的bounds之外,那么这个view及其它的子view将会被忽略。因此,当一view的clipsToBounds=NO,如果触摸事件发生在子view上超出父视图的部分,那么
hitTest:withEvent也不会将这个子view返回。
下面利用一个demo来验证一下第一响应者查找的过程。

视图的层级如下:
Window
└── ViewA
├── ViewB
└── ViewC
├── ViewD
└── ViewE
现在点击ViewD,则相应的打印如下图:
🧪 =======》Window, Begin handling hitTest
📍 ===》Window, Begin handling pointInside
📍《=== Window, handing pointInside, result: true
🧪 =======》ViewA, Begin handling hitTest
📍 ===》ViewA, Begin handling pointInside
📍《=== ViewA, handling pointInside ViewA: true
🧪 =======》ViewC, Begin handling hitTest
📍 ===》ViewC, Begin handling pointInside
📍《=== ViewC, handling pointInside ViewC: true
🧪 =======》ViewE, Begin handling hitTest
📍 ===》ViewE, Begin handling pointInside
📍《=== ViewE, handling pointInside ViewE: false
🧪《======= ViewE, handling hitTest, result: nil
🧪 =======》ViewD, Begin handling hitTest
📍 ===》ViewD, Begin handling pointInside
📍《=== ViewD, handling pointInside ViewD: true
🧪《======= ViewD, handling hitTest, result: ViewD
🧪《======= ViewC, handling hitTest, result: ViewD
🧪《======= ViewA, handling hitTest, result: ViewD
🧪《======= Window, handing hitTest, result: ViewD
/* 重复打印上面的操作,为了二次确认*/
🔥 Window, handing sendEvent in Window
✅ First responder: Optional("ViewD")
🔥 ViewD, handling touchesBegan in ViewD
🔥 ViewC, handling touchesBegan in ViewC
🔥 ViewA, handling touchesBegan in ViewA
🔥 Window, handing touchesBegan in Window
🔥 Window, handing sendEvent in Window
结果分析:
- 1.首先从
Window开始,先调用hitTest:withEvent:,然后调用pointInside:withEvent:,因为触摸点确实在Window上所以pointInside:withEvent:返回了true - 2.然后遍历
Window的子视图ViewA, 调用hitTest:withEvent:,然后调用pointInside:withEvent:,因为触摸点确实在ViewA上所以pointInside:withEvent:返回了true - 3.然后遍历
ViewA的子视图(ViewB,ViewC),遍历采用的是从后往前(也即先遍历最后addSubview的视图,因为最后添加的视图通常情况下是在视图的最上层,苹果这里做了一个优化)。先遍历ViewC,操作同Window。 - 4.然后遍历
ViewC的子视图(ViewD,ViewE)。同样,遍历采用的是从后往前。先遍历ViewE,操作同Window.这时pointInside:withEvent:返回了false,也即不包含触摸点。那么相应的hitTest:withEvent返回了null - 5.遍历ViewD,因为ViewD已经是一个叶节点(iOS中视图的层级是一个n杈树),其
pointInside:withEvent:返回了true,与此相对应的hitTest:withEvent返回最终包含触摸点的视图,也即第一响应者。hitTest:withEvent的查找类似一个递归,所以在包含第一响应者的分支上的每一个视图中的hitTest:withEvent都返回第一个响应者。 - 6.因为已经找到了第一响应者,所以ViewB这个分支就没必要遍历了。
🔥官方文档明确指出,下面情况的视图不在遍历范围内:
- 视图的
hidden等于YES。 - 视图的
alpha小于等于0.01。 - 视图的
userInteractionEnabled为NO。
也就是说在hitTest:withEvent调用过程中会进行上面三种情况的判断。
🔥 总结
hitTest(_:with:)
→ point(inside:with:)
→ 子视图倒序遍历
→ 返回唯一一个 View
伪代码大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//3种状态无法响应事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//触摸点若不在当前视图上则无法响应事件
if ([self pointInside:point withEvent:event] == NO) return nil;
//从后往前遍历子视图数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 获取子视图
UIView *childView = self.subviews[I];
// 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
CGPoint childP = [self convertPoint:point toView:childView];
//询问子视图层级中的最佳响应视图
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
//如果子视图中有更合适的就返回
return fitView;
}
}
//没有在子视图中找到更合适的响应视图,那么自身就是最合适的
return self;
}
在找到第一响应者之后所有的信息全部包含在一个UIEvent对象中,接下来会通过sendEvent方法直接将事件传递给第一响应者(在上面的操作中是直接传递给ViewD)。

// 在 windows中重新sendEvent时候不要忘记写super,否则所有触摸事件都不生效
// 在这里可以遍历获取对应的触摸对象
- (void)sendEvent:(UIEvent *)event {
NSLog(@"🔥 sendEvent in %@", NSStringFromClass([self class]));
NSSet<UITouch *> *touches = [event allTouches];
if (touches) {
for (UITouch *touch in touches) {
if (touch.phase == UITouchPhaseBegan) {
UIView *view = touch.view;
NSLog(@"✅ First responder: %@", NSStringFromClass([view class]));
}
}
}
[super sendEvent: event];
}
四、使用场景
场景 1:扩大按钮的点击或缩小区域
class BigTouchButton: UIButton {
// 四周多了20的边距
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let biggerArea = bounds.insetBy(dx: -20, dy: -20)
return biggerArea.contains(point)
}
}
场景 2:处理蒙层是否可点击
class PopupHitTestView: UIView {
weak var delegate: PopupHitTestViewDelegate?
private var ignoreHit: Bool = false
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.ignoreHit {
return nil
}
var hitView = super.hitTest(point, with: event)
//如果点中的是maskview自己, 否则原样返回
if hitView == self {
self.ignoreHit = true
// 利用window二次确认是否点击了maskview
hitView = window?.hitTest(point, with: event)
self.ignoreHit = false
var shouldHit = false
if let hitView = hitView {
shouldHit = self.delegate?.popupHitTestView(self, shouldHit: hitView) ?? false
}
if shouldHit {
return hitView
} else {
return self
}
} else {
return hitView
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
delegate?.popupHitTestViewTouched(self)
}
}
场景 3:缩小点击区域
class PartialTouchView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
// 处理右半部分区域
if let hitView, hitView == self {
if point.x > hitView.bounds.width / 2.0 && point.x <= hitView.bounds.width {
return nil
} else {
print("点击了左半部分区域")
}
}
return hitView
}
}
场景 4:让 A View 替 B View 接收点击(同级或者不同级事件转发)
class RedirectTouchView: UIView {
weak var targetView: UIView?
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView == self {
return targetView
}
return hitView
}
}
class TargetTouchView: UIView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("接收事件")
}
}
场景 5:Debug 打印到底谁执行了事件
class DebugHitTestView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let v = super.hitTest(point, with: event)
print("✅ 命中视图:\(String(describing: v))")
return v
}
}