事件传递和响应者链
文章目录
- 概述
- UIEvent
- UITouch
- 手势识别器与Touch
- UIResponder
- 事件的传递
- 第一响应者
- 殴打视图直到它吐出靠前的视图(Hit-testing)
- 响应者链
- Target-Action 机制
概述
在使用手机的过程中,会产生很多的交互事件,如触摸屏幕,摇晃,按下按键,使用耳机等外接设备操控设备等,这些事件都需要系统去响应并处理,这篇文章简单将简单介绍系统如何响应,处理事件
UIEvent
UIEvent 描述用户与Apple的单次交互
应用可以接收不同类型的事件,包括触摸事件(touch events)、运动事件(motion events)、远程控制事件(remote-control events)和按下物理按键事件(press events)
触摸事件是最常见的事件,一般发送给触摸的视图
运动时间由UIKit触发,并与 Core Motion framework 运动事件进行区分
远程控制事件允许响应者对象接收外部设备的指令
按下按键代表软件可能与手柄或其他有物理按键设备的交互
触摸事件对象包含与事件相关的Touch,Touch Event对象包含一个或者多个UITouch对象
当发生触摸事件的时候,系统将事件交给合适的响应者,并调用touchesBegan(_:with:)方法,responder 提取 touch 中的数据,并作出适当响应
注意!!!
在更新触摸数据(即位置,面积等数据)时,UIKit会复用UIEvent对象
因此不要通过UIEvent 对象来读取你需要的数据,如果有必要使用UIEvent UITouch类的数据,应该在响应者方法中处理数据,并复制到自定义的数据结构里
UITouch
在UIEvent 的介绍里,我们提到了多次UITouch 类对象,这里我们详细讲一下UITouch 类
UITouch对象表示屏幕上 touch 的位置、大小、移动和压力。
通过传递给 responder 的UIEvent来获取UITouch对象,UITouch提供以下信息:
- touch 发生的 view 或 window
- touch 位于 view 或 window 的位置
- touch 大致半径
- touch 压力大小(支持 3D Touch 或 Apple Pencil 的设备,悲哀的是 3D Touch由于各种奇奇怪怪的原因已经无了)
另外,touch 对象使用timestamp属性表示 touch 发生的时间,该时间为马赫时间。
另外的另外,timestamp记录的是事件发生(.began)、改变(.move)的时间,事件发生与传递可能有delay,使用时需对比前后事件的timestamp,而非UTC时间

整数类型的tapCount属性表示点击屏幕的次数,UITouch.Phase属性表示处于began、moved、ended、cancelled等阶段

Touch 对象会在整个多点触控序列中存在。当处理多点触控序列时,可以引用 touch 对象,直到触控结束才释放。如果需要在多点触控序列外使用,需复制 touch 中的数据到自定义数据结构

estureRecognizers属性包含了当前处理 touch 的手势识别器。UIGestureRecognizer实现了touchesBegan(_:with:)、touchesMoved(_:with)、touchesEnded(_:with)、touchesCancelled(_:with)四个方法
但它其并不是responder,也不参与响应链
在touch传递给view时,也会传递给view关联的gesture recognizer
如果view的父视图包含手势识别器,也会传递给父视图手势识别器。最终,传递给整个视图层级中的手势识别器
手势识别器与Touch
当touch首次创建并传递给gesture recognizer时,也会传递给hit-test视图,同时调用视图、手势识别器的touchesBegan(_:with:)方法
这样就不会因为手势识别器正在分析手势导致View接收不到时事件了。如果所有的手势识别器都识别失败,则视图会继续接收事件
如果一个View的手势识别器识别成功,会给view发送touchesCancelled(_:with:)消息,随后该view不会再收到该touch事件。可以通过修改cancelsTouchesInView属性为false可以改变这一特点
UIResponder
UIResponder类抽象了响应和处理事件的接口
响应者构成了事件处理的骨干,很多对象都继承于UIResponder ,如UIApplication、UIViewController、UIView
当事件发生的时候,UIKit 调度事件给responder 处理
想要处理特定类型事件,响应者自身需要重写对应的方法
比较常见的有:
- touchesBegan(_:with:)
- touchesMoved(_:with:)
- touchesEnded(_:with:)
- touchesCancelled(_:with:)
响应者对象除了可以处理UIEvent ,还可以通过inputView 接收自定义的输入,系统的键盘就是一种inputView
当用户点击屏幕上的UITextField 的时候,他成为第一响应者并显示inputView默认展示系统键盘
你可以创建自定义的inputView来赋值给inputView属性,当其成为第一响应者时展示
事件的传递
这里讲一下当iPhone接收到一个事件的时候的处理过程,很多东西我自己也没全部理解,但是了解一下总归是好的
处理过程如下:
-
通过动作触发事件唤醒处于睡眠状态的App
-
使用IOKit.framework 将事件封装为 IOHIDEvent 对象
IOKit.framework 是 Apple 操作系统的硬件中枢。它确保了操作系统能够识别、管理并与所有硬件设备高效、安全地通信。IOHIDEvent 中的 HID 代表 Human Interface Device
-
系统通过 mach port 将 IOHIDEvent 对象转发给 SpringBoard.app。
-
SpringBoard.app 是 iOS 系统桌面 app,只接收按键、触摸、加速、接近传感器等几种 event。SpringBoard.app 找到可以响应这个事件的 app,并通过 mach port 将 IOHIDEvent 对象转发给 app 。
-
app 主线程 RunLoop 接收到 SpringBoard.app 转发的消息,触发对应 mach port 的 source1 回调 __IOHIDEventSystemClientQueueCallback()。
-
Source1 回调内部触发 Source0 回调 __UIApplicationHandleEventQueue()。
-
Source0 回调内部,将 IOHIDEvent 对象转化为
UIEvent。 -
Source0 回调内部调用 UIApplication 的
sendEvent(_:)方法,将UIEvent发给UIWindow。
UIWindow接收到事件后,开始传递事件。
第一响应者
第一响应者的概念在开发中是更为常见的一个概念
下图显示了包含UILabel、UITextField、UIButton、UIView、UIViewController、UIWindow等视图的事件传递

如果文本框没有处理 event,UIKit 转发事件给文本框的父视图UIView,随后是控制器的根视图、视图控制器、window。如果 window 也没有处理 event,UIKit 转发 event 至 UIApplication
UIKit 根据事件类型指定第一响应者,事件类型如下:
| 事件类型 | 第一响应者 |
|---|---|
| 触摸事件 | 触摸的视图 |
| 按压事件 | 焦点对象 |
| 晃动事件 | 开发者或UIKit指定的对象 |
| 远程控制事件 | 开发者或UIKit指定的对象 |
| 编辑按钮信息 | 开发者或UIKit指定的对象 |
与加速度计、陀螺仪和磁力计相关的运动事件不跟随响应链。相反,Core Motion 会将这些事件直接传递给指定的对象。有关更多信息,请参阅 Core Motion Framework
触摸事件传递大致分为三个阶段:
- 寻找触摸对象(通过击打测试)
- 响应手势(手势识别器)
- 触摸事件传递(响应者链)
我们接下来逐个讲解
殴打视图直到它吐出靠前的视图(Hit-testing)
Hit-testing 是查找 touch point 是否位于指定视图上的过程。iOS 使用 hit-testing 查找触摸事件最前的视图(即视图数组中 index 最大的视图),hit-testing 使用逆序深度优先遍历算法(reverse pre-order depth-first traversal algorithm)查找视图
在开始讲解击打测试之前,先看一下手指触摸屏幕到抬起的单次触摸流程

可以忽略其他的过程,我们现在只需要关注击打测试
注意到每次触摸屏幕,都会调用hit-testing,并且是在视图、gesture recognizer 收到UIEvent之前
Hit-testing 结束后,触摸位置下最前端视图被选为 first responder,它被关联到UITouch对象,并且 touch event 的所有阶段都会关联此视图
除了 hit test 视图,添加到 hit-test 视图上的视图、父视图上的手势识别器,都会关联到UITouch对象
最后,hit test 视图开始接受触摸系列事件
注意!!!
即使你的手指可能已经滑动到hit-test View的边界之外,到了另一个视图上,你的hit-Test View也会继续接收UITouch 直到你的touch-event结束
为了方便理解我们给出这么一个视图树,包括显示和视图层级树

逆序深度优先遍历算法查找过程如下:

先从根视图开始,先向UIWindow发送hitTest(_:with:)消息,该方法会返回包含触摸位置的视图
下图给出查找逻辑的流程图

在UIWindow 调用了自己的Hit-Test方法后,他会先判断自己是否可以被点击,主要检查三方面:
- 自己是否可以被交互(userInteractionEnabled)
- 自己是否可见(hidden和不透明度)
- 触摸点是否在自己内部(pointInside:point方法)
在这些条件全部为真的时候,会遍历自己的子视图数组。由于后加入子视图数组的子视图的Z轴高度一定会高于先加入的(即addsubView逻辑,后加入的会被显示在靠前的位置),所以使用倒叙遍历
对每个遍历的子视图,会进行两个步骤
- 转换点坐标,将点的坐标转化为子视图的
- 对子视图递归调用hit-Test
递归调用后,新子视图会重复这个过程,即先进行自我检查,之后检查自己的子视图
手势识别器部分会在之后重新
响应者链
事件传递链是事件向下传递的方法,即依赖于hitTest:withEvent:和pointInside:withEvent:
事件传递链为由上而下,即从UIApplication实例到最终视图UIView
最终视图即通过击打测试的视图,就是事件响应链开始
事件响应链决定链如果这个最终视图不想或者不能完全处理这个事件,事件应该向上传递给谁来处理
我们在之前的内容中提到了UIResponder 类,每一个UIResponder实例都有一个nextResponder属性,它指向链条中的下一个响应者,向上寻找最适合的响应对象
在实际应用上,我们会一般会重写touch:Begin方法,让响应者链断在合适的View(不调用super )
Target-Action 机制
像UIButton 这类的控件,其Target-Action 机制也利用了响应者链,如果将target设置为nil,系统就会沿着响应者链查找第一个实现Action方法的响应者对象,并向其发送该消息
