iOS开发基础29-事件处理深度解析:事件类型、响应者链条、Hit-Test 与手势识别
iOS 事件处理深度解析:事件类型、响应者链条、Hit-Test 与手势识别
本文系统梳理 iOS 事件处理机制:三大事件类型、UIResponder 响应者对象、触摸事件与 UITouch/UIEvent、事件产生与 Hit-Test 传递机制、响应者链条、手势识别器及其与触摸事件的关系。
一、iOS 中的事件类型
iOS 事件分为三大类型:
| 事件类型 | 说明 | 触发场景 |
|---|---|---|
触摸事件(UIEventTypeTouches) |
用户触摸屏幕产生 | 点击、滑动、捏合、旋转等 |
运动事件(UIEventTypeMotion) |
设备运动产生 | 摇一摇(UIEventSubtypeMotionShake) |
远程控制事件(UIEventTypeRemoteControl) |
外部控制产生 | 耳机线控、控制中心播放/暂停/切歌 |
运动事件中,UIResponder 的
motionBegan/motionEnded主要用于摇一摇。更复杂的加速度计、陀螺仪、磁力计数据通过 Core Motion 框架(CMMotionManager)获取,不经过 UIResponder 事件链。
二、响应者对象(Responder Object)
只有继承自 UIResponder 的对象才能接收和处理事件,称为响应者对象。
UIResponder
├── UIView
│ ├── UIWindow
│ ├── UIControl(UIButton、UITextField 等)
│ ├── UILabel(默认 userInteractionEnabled = NO,不响应触摸)
│ └── UIImageView(默认 userInteractionEnabled = NO,不响应触摸)
├── UIViewController
└── UIApplication
UILabel和UIImageView默认userInteractionEnabled = NO,无法响应触摸事件。如需响应,需手动设置userInteractionEnabled = YES。
第一响应者
第一响应者(First Responder)是当前接收事件的响应者对象,如正在编辑的 UITextField。
// 成为第一响应者(如弹出键盘)
[textField becomeFirstResponder];
// 辞去第一响应者(如收起键盘)
[textField resignFirstResponder];
// 判断是否是第一响应者
BOOL isFirst = [textField isFirstResponder];
三、UIResponder 事件处理方法
UIResponder 提供了处理三类事件的方法,子类可重写:
触摸事件
- (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)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
摇一摇事件需要视图控制器或视图成为第一响应者(
becomeFirstResponder),且canBecomeFirstResponder返回 YES。
四、UIView 的触摸事件处理
UIView 继承自 UIResponder,可重写触摸方法:
@interface CustomView : UIView
@end
@implementation CustomView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"触摸开始");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"触摸移动");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"触摸结束");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"触摸取消(如来电、手势识别成功)");
}
@end
重写触摸方法时,建议调用
[super touchesBegan:...](如果需要事件继续沿响应者链条传递)。不调用 super 则事件被当前视图"吃掉",不再向上传递。
五、UITouch 对象
每根手指触摸屏幕时创建一个 UITouch 对象,一根手指对应一个 UITouch,在整个触摸过程中保持同一个对象。
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
window |
UIWindow |
触摸产生时所处的窗口 |
view |
UIView |
触摸产生时所处的视图 |
tapCount |
NSUInteger |
连续点击次数(区分单击/双击) |
timestamp |
NSTimeInterval |
触摸事件的时间戳 |
phase |
UITouchPhase |
触摸阶段(Began/Moved/Stationary/Ended/Cancelled) |
force |
CGFloat |
按压力度(3D Touch,iOS 9+) |
maximumPossibleForce |
CGFloat |
最大可能按压力度 |
常用方法
// 获取触摸点在指定视图中的位置(坐标相对于 view 的左上角)
CGPoint location = [touch locationInView:self.view];
// 获取上一个触摸点的位置(用于计算移动距离)
CGPoint previousLocation = [touch previousLocationInView:self.view];
UITouchPhase 枚举
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // 手指刚接触屏幕
UITouchPhaseMoved, // 手指在屏幕上移动
UITouchPhaseStationary, // 手指停留在屏幕上(未移动)
UITouchPhaseEnded, // 手指离开屏幕
UITouchPhaseCancelled, // 触摸被取消(来电、系统中断、手势识别成功)
};
六、UIEvent 对象
每产生一个事件,系统创建一个 UIEvent 对象,记录事件的类型、子类型、时间戳和所有触摸对象。
常用属性
| 属性 | 说明 |
|---|---|
type |
事件类型(UIEventTypeTouches/Motion/RemoteControl/Presses) |
subtype |
事件子类型(如 UIEventSubtypeMotionShake) |
timestamp |
事件时间戳 |
allTouches |
事件包含的所有 UITouch 对象 |
常用方法
// 获取指定视图上的所有触摸对象
NSSet<UITouch *> *touches = [event touchesForView:self.view];
// 获取指定窗口上的所有触摸对象
NSSet<UITouch *> *windowTouches = [event touchesForWindow:self.view.window];
七、事件的产生与传递(Hit-Test)
1. 事件产生与分发流程
用户触摸屏幕
↓
系统(IOKit)识别触摸,封装为 UIEvent
↓
UIApplication 从事件队列取出事件
↓
分发给 keyWindow(主窗口)
↓
keyWindow 调用 hitTest:withEvent: 查找最合适的视图
↓
找到的视图成为事件的第一响应者,调用其 touches 方法
2. hitTest:withEvent: 底层实现
hitTest:withEvent: 是 UIView 的方法,系统在事件分发时调用,返回最合适处理事件的视图:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1. 判断当前视图能否接收事件
if (self.userInteractionEnabled == NO || // 禁止交互
self.hidden == YES || // 隐藏
self.alpha <= 0.01) { // 几乎透明
return nil;
}
// 2. 判断触摸点是否在当前视图范围内
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
// 3. 从后往前遍历子视图(后添加的子视图在层级上方,优先响应)
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
// 将触摸点坐标转换到子视图坐标系
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
// 递归调用子视图的 hitTest
UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
if (hitView) {
return hitView; // 子视图能处理,返回子视图
}
}
// 4. 没有子视图能处理,自己处理
return self;
}
3. pointInside:withEvent:
pointInside:withEvent: 判断触摸点是否在视图范围内,可重写来扩大点击区域:
// 扩大按钮点击区域(点击范围比可视范围大 20pt)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect bounds = self.bounds;
CGFloat widthDelta = MAX(44.0 - bounds.size.width, 0);
CGFloat heightDelta = MAX(44.0 - bounds.size.height, 0);
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
return CGRectContainsPoint(bounds, point);
}
Apple 推荐可点击控件的最小点击区域为 44x44pt,小于此尺寸的按钮可通过重写
pointInside:withEvent:扩大点击区域。
八、响应者链条
响应者链条(Responder Chain)是由多个响应者对象按顺序连接起来的链条,事件可沿链条向上传递,让多个对象有机会处理同一个事件。
1. 完整响应者链条
当前视图(hitTest 找到的视图)
↓(如果不处理或调用 super)
父视图(superview)
↓
...(逐级向上)
↓
UIWindow
↓
UIApplication
↓
UIApplication 的 delegate(AppDelegate,如果是 UIResponder 子类)
2. 上一个响应者(nextResponder)的判断
| 当前对象 | 上一个响应者 |
|---|---|
| 普通 UIView | 父视图(superview) |
| UIViewController 的 view | 该 UIViewController |
| UIViewController | 父视图控制器 或 UIWindow(如果是根控制器) |
| UIWindow | UIApplication |
| UIApplication | AppDelegate(如果是 UIResponder 子类) |
3. 事件沿响应者链条传递的完整过程
1. 事件由上往下传递(hitTest),找到最合适的视图
↓
2. 调用最合适视图的 touchesBegan/Moved/Ended
↓
3. 如果该视图调用了 [super touches...],事件传递给上一个响应者
↓
4. 上一个响应者的 touches 方法被调用
↓
5. 重复 3-4,直到某个响应者处理了事件(不调用 super)或到达链条末端
↓
6. 到达 UIApplication/AppDelegate 仍未处理,事件被丢弃
默认情况下,
UIView的touchesBegan等方法不调用 super(事件不向上传递)。UIControl(如 UIButton)通过 target-action 机制处理事件,也不沿响应者链条传递。
九、手势识别(UIGestureRecognizer)
iOS 3.2 引入手势识别器,将触摸事件的识别逻辑封装,大大简化了复杂手势的处理。
1. 常见手势识别器
| 手势识别器 | 说明 |
|---|---|
UITapGestureRecognizer |
点按(单击/双击/多击) |
UIPinchGestureRecognizer |
捏合(缩放) |
UIPanGestureRecognizer |
拖拽(平移) |
UISwipeGestureRecognizer |
轻扫(快速滑动) |
UIRotationGestureRecognizer |
旋转 |
UILongPressGestureRecognizer |
长按 |
2. 使用步骤
// 1. 创建手势识别器
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
// 2. 设置属性
tap.numberOfTapsRequired = 2; // 双击
tap.numberOfTouchesRequired = 1; // 一根手指
// 3. 添加到视图(视图的 userInteractionEnabled 必须为 YES)
[self.view addGestureRecognizer:tap];
// 4. 实现手势处理方法
- (void)handleTap:(UITapGestureRecognizer *)tap {
NSLog(@"双击手势识别成功");
}
一个手势识别器只能添加到一个视图,但一个视图可以添加多个手势识别器。
3. 各种手势示例
长按
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
longPress.minimumPressDuration = 1.0; // 最短按 1 秒
[self.view addGestureRecognizer:longPress];
- (void)handleLongPress:(UILongPressGestureRecognizer *)longPress {
if (longPress.state == UIGestureRecognizerStateBegan) {
NSLog(@"长按开始");
}
}
轻扫
// 一个 UISwipeGestureRecognizer 实例只能识别一个方向,需要多个方向需创建多个实例
UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
swipeRight.direction = UISwipeGestureRecognizerDirectionRight;
[self.view addGestureRecognizer:swipeRight];
UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft;
[self.view addGestureRecognizer:swipeLeft];
- (void)handleSwipe:(UISwipeGestureRecognizer *)swipe {
if (swipe.direction == UISwipeGestureRecognizerDirectionRight) {
NSLog(@"向右轻扫");
} else if (swipe.direction == UISwipeGestureRecognizerDirectionLeft) {
NSLog(@"向左轻扫");
}
}
旋转
UIRotationGestureRecognizer *rotation = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotation:)];
[self.imageView addGestureRecognizer:rotation];
- (void)handleRotation:(UIRotationGestureRecognizer *)rotation {
// rotation.rotation 是自手势开始以来的累计旋转角度(弧度)
self.imageView.transform = CGAffineTransformRotate(self.imageView.transform, rotation.rotation);
rotation.rotation = 0; // 重置,避免累计
}
捏合(缩放)
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
[self.imageView addGestureRecognizer:pinch];
- (void)handlePinch:(UIPinchGestureRecognizer *)pinch {
// pinch.scale 是自手势开始以来的累计缩放比例
self.imageView.transform = CGAffineTransformScale(self.imageView.transform, pinch.scale, pinch.scale);
pinch.scale = 1.0; // 重置,避免累计
}
拖拽(平移)
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[self.dragView addGestureRecognizer:pan];
- (void)handlePan:(UIPanGestureRecognizer *)pan {
CGPoint translation = [pan translationInView:self.view];
self.dragView.center = CGPointMake(self.dragView.center.x + translation.x,
self.dragView.center.y + translation.y);
[pan setTranslation:CGPointZero inView:self.view]; // 重置
}
4. 手势识别状态
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // 可能识别(等待触摸事件)
UIGestureRecognizerStateBegan, // 手势开始(连续手势)
UIGestureRecognizerStateChanged, // 手势变化(连续手势)
UIGestureRecognizerStateEnded, // 手势结束(连续手势)/ 识别成功(离散手势)
UIGestureRecognizerStateCancelled, // 手势取消
UIGestureRecognizerStateFailed, // 手势识别失败
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
| 手势类型 | 状态序列 |
|---|---|
| 离散手势(Tap/Swipe) | Possible → Ended(或 Failed) |
| 连续手势(Pan/Pinch/Rotation/LongPress) | Possible → Began → Changed → Ended(或 Cancelled/Failed) |
5. 手势识别与触摸事件的关系
手势识别器优先于视图的触摸事件处理:
触摸事件产生
↓
系统先将事件发送给视图的手势识别器
↓
手势识别器识别中 → 视图收到 touchesBegan(取决于 delaysTouchesBegan)
↓
手势识别成功 → 向视图发送 touchesCancelled(取消视图的触摸处理)
↓
手势识别失败 → 视图收到 touchesEnded(继续处理触摸事件)
控制手势与触摸事件关系的属性
| 属性 | 默认值 | 说明 |
|---|---|---|
cancelsTouchesInView |
YES |
手势识别成功后,向视图发送 touchesCancelled,取消视图的触摸处理 |
delaysTouchesBegan |
NO |
是否延迟 touchesBegan 到手势识别失败后(YES 时视图不会立即收到 touchesBegan) |
delaysTouchesEnded |
YES |
是否延迟 touchesEnded 到手势识别失败后(YES 时手势识别失败后视图才收到 touchesEnded) |
6. 多手势同时识别
默认情况下,一个视图上的多个手势识别器互斥(一个识别成功后其他失败)。通过 UIGestureRecognizerDelegate 可支持同时识别:
@interface ViewController () <UIGestureRecognizerDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
pinch.delegate = self;
rotation.delegate = self;
}
// 允许多个手势同时识别(如同时缩放和旋转)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
@end
7. 手势依赖关系
通过 requireGestureRecognizerToFail: 设置手势的依赖关系(一个手势必须等另一个手势失败后才能识别):
// 双击手势必须等单击手势失败后才能识别(避免单击和双击冲突)
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] init];
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] init];
doubleTap.numberOfTapsRequired = 2;
// 单击手势需要双击手势失败后才能识别
[singleTap requireGestureRecognizerToFail:doubleTap];
十、Swift 版本对照
触摸事件
class CustomView: UIView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
print("触摸开始")
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let previous = touch.previousLocation(in: self)
print("移动: \(location), 上次: \(previous)")
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("触摸结束")
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
print("触摸取消")
}
}
Hit-Test
class CustomView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard isUserInteractionEnabled, !isHidden, alpha > 0.01 else { return nil }
guard point(inside: point, with: event) else { return nil }
for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
if let hitView = subview.hitTest(convertedPoint, with: event) {
return hitView
}
}
return self
}
// 扩大点击区域
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let inset = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
return bounds.inset(by: inset).contains(point)
}
}
手势识别
class ViewController: UIViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// 点按
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
tap.numberOfTapsRequired = 2
view.addGestureRecognizer(tap)
// 拖拽
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
view.addGestureRecognizer(pan)
// 捏合
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
pinch.delegate = self
view.addGestureRecognizer(pinch)
// 旋转
let rotation = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:)))
rotation.delegate = self
view.addGestureRecognizer(rotation)
}
@objc func handleTap(_ gesture: UITapGestureRecognizer) {
print("双击")
}
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: view)
// 移动视图...
gesture.setTranslation(.zero, in: view)
}
@objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
view.transform = view.transform.scaledBy(x: gesture.scale, y: gesture.scale)
gesture.scale = 1.0
}
@objc func handleRotation(_ gesture: UIRotationGestureRecognizer) {
view.transform = view.transform.rotated(by: gesture.rotation)
gesture.rotation = 0
}
// 多手势同时识别
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
十一、总结
- 事件类型:触摸事件、运动事件(摇一摇)、远程控制事件。复杂传感器数据通过 Core Motion 获取,不经过 UIResponder。
- 响应者对象:继承 UIResponder 的对象才能处理事件;UILabel/UIImageView 默认 userInteractionEnabled=NO 不响应触摸。
- UITouch:每根手指对应一个对象,
locationInView:/previousLocationInView:获取位置,phase表示触摸阶段,force表示按压力度。 - UIEvent:记录事件类型、子类型、时间戳和所有触摸对象。
- Hit-Test:事件从 UIApplication → keyWindow,通过
hitTest:withEvent:从上往下递归查找最合适的视图;判断条件为 userInteractionEnabled/hidden/alpha 和 pointInside;从后往前遍历子视图。 - 响应者链条:view → superview → ... → UIWindow → UIApplication → AppDelegate;调用
[super touches...]可将事件向上传递。 - 手势识别:6 种手势识别器,优先于视图触摸事件处理;识别成功后默认向视图发送 touchesCancelled;通过 delegate 支持多手势同时识别,通过 requireGestureRecognizerToFail 设置依赖关系。
- 手势与触摸关系:
cancelsTouchesInView/delaysTouchesBegan/delaysTouchesEnded控制手势与视图触摸事件的交互。

浙公网安备 33010602011771号