响应者对象就是可以响应事件并对事件作出处理。在 iOS 中,存在 UIResponder 类,它定义了响应者对象的所有方法。UIApplication、UIView 等类都继承了 UIResponder 类,UIWindow 和 UIKit 中的控件因为继承了 UIView,所以也间接继承了 UIResponder 类,这些类的实例都可以当作响应者。
当前接受触摸的响应者对象被称为第一响应者,即表示当前该对象正在与用户交互,它是响应者链的开端。
响应者链表示一系列的响应者对象。事件被交由第一响应者对象处理,如果第一响应者不处理,事件被沿着响应者链向上传递,交给下一个响应者。
一般来说,第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理,事件就会被传递给它的视图控制器对象(如果存在),然后是它的父视图对象(如果存在),以此类推,直到顶层视图。
接下来会沿着顶层视图(top view)到窗口(UIWindow)再到程序(UIApplication)。如果整个过程都没有响应这个事件,该事件就被丢弃。
一般情况下,在响应者链中只要由对象处理事件,事件就停止传递。但有时候可以在视图的响应方法中根据一些条件判断来决定是否需要继续传递事件。
事件分发
视图对触摸事件是否需要作处回应可以通过设置视图的 userInteractionEnabled 属性。默认状态为 YES,如果设置为 NO,可以阻止视图接收和分发触摸事件。除此之外,当视图被隐藏或者透明也不会接收事件。
不过这个属性只对视图有效,如果想要整个程序都不响应事件,可以调用 UIApplication 的 beginIngnoringInteractionEvents 方法来完全停止事件接收和分发。通过 endIngnoringInteractionEvents 方法来恢复让程序接收和分发事件。
如果要让视图接收多点触摸,需要设置它的 multipleTouchEnabled 属性为 YES,默认状态下这个属性值为NO,即视图默认不接收多点触摸,整个 iOS 触摸事件从产生到寂灭大致如下图:

起始阶段
- CPU 处于睡眠状态,等待事件发生;
- 手指触摸屏幕
系统响应阶段
- 屏幕硬件感应到输入,并将感应到的事件传递给输入输出驱动
IOKit; IOKit.framework封装整个触摸事件为IOHIDEvent对象;IOKit.framework通过 IPC 将事件转发给SpringBoard.app;
SpringBoard.app就是 iOS 的系统桌面,当触摸事件发生时,也只有负责管理桌面的SpringBoard.app才知道如何正确的响应。因为触摸发生时,有可能用户正在桌面翻页找 App,也有可能正处于在微信中刷朋友圈。
以上是系统层的响应,系统感应到外界的输入,并将相应的输入封装成比较概括的 IOHIDEvent 对象,然后 UIKit 通过 IOHIDEvent 的类型,判断出相应事件应该由 SpringBoard .app 处理,直接通过 Mach Port(IPC进程间通信) 转发给 SpringBoard.app。
桌面响应阶段
SpringBoard.app 主线程 Runloop 收到 IOKit.framework 转发来的消息苏醒,并触发对应 Mach Port 的 Source1 回调 __IOHIDEventSystemClientQueueCallback()。
如果 SpringBoard.app 监测到有 App 在前台,即 xxxx.app,SpringBoard.app 通过 Mach Port(IPC进程间通信) 转发给 xxxx.app,如果 SpringBoard.app 监测到监测无前台 App,则 SpringBoard.app 进入 App 内部响应阶段的第二段,即触发 Source0 回调。
Source1 事件响应
苹果注册了一个 Source1 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件,如触摸/锁屏/摇晃等发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,距离传感器等几种 Event,随后用 Mach Port 转发给需要的 App 进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture,处理屏幕旋转,发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
Source0 手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
App内部响应阶段
- 前台 App 主线程
Runloop收到SpringBoard.app转发来的消息苏醒,并触发对应Mach Port的Source1回调__IOHIDEventSystemClientQueueCallback(); Source1回调内部触发Source0回调__UIApplicationHandleEventQueue();Soucre0回调内部,封装IOHIDEvent为UIEvent;- 平时开发熟悉的触摸事件响应链从这开始了;
- 通过递归调用
UIView层级的hitTest(_:with:),结合point(inside:with:)找到UIEvent中每一个UITouch所属的UIView,其实是想找到离触摸事件点最近的那个UIView。 - 这个过程是从
UIView层级的最顶层往最底层递归查询,但这不是UIResponder响应链,事件响应是在UIEvent中每一个UITouch所属的UIView都确定之后方才开始。
但需要注意,以下三种情况 UIView 的 hitTest(_:with:) 不会被调用,也导致其子 UIView 的 hitTest(_:with:) 不会被调用,而之后响应事件是下向上传递的,这直接导致以下三种情况的 UIView 及其子 UIView 不接收任何触摸事件:
- userInteractionEnabled = NO
- hidden = YES
- alpha = 0.0~0.01之间
UIImageView 的 userInteractionEnabled 默认为NO,因此 UIImageView 以及它的子控件默认是不接收触摸事件的。
当把断点打在某个 UIView hitTest(_:with:) 中时,对应的调用堆栈如下:
根据围绕
UITouch所属的UIView及其父视图UIView的UIGestureRecognizer,来确定一个UITouch的UIGestureRecognizer;UITouch所属的UIView和gestureRecognizers收到此UITouch和相应的UIEvent,并按照UITouch所处的状态调用四大UITouch方法中的一个,事件响应开始;对于
UIView收到的UITouch事件,四大UITouch事件都是如此,则会按照UIResponder响应链一直往上传递,直到某个UIResponder因为主动响应触摸事件,切断了响应链,即不调用下一个UIResponder的响应方法,如果一直没有UIResponder做响应处理,则这些UITouch到达最后的响应者即UIApplication后,就被吃掉消失。如果在事件响应过程中,有
UIGestureRecognizer成功识别,则此UIGestureRecognizer将独自占有所需要的UITouch,这些UITouch所属的UIView及其他的UIGestureRecognizer的touchesCancelled(_:with:)方法将调用。如果在手势的代理中设置可以同时识别两个手势,则允许同时识别的手势均可以收到所需要的
UITouch事件,但与识别成功的UIGestureRecognizer无关的UITouch则会继续按照上述传递逻辑传递。也即允许两个手势同时识别,只要所占有的UITouch不相同。如果
UIGestureRecognizer识别成功,则调用相应的 action,处理对应的逻辑。如果某个UIResponder主动响应了触摸事件,则根据其本身的响应逻辑处理对应的业务,UIControl都是主动响应并切断UITouch的向上传递的。UITouch事件流动完毕,整个系统重新进入睡眠等待下一个事件。
响应者链
通常,一个 iOS 应用中,在一块屏幕上通常有很多的 UI 控件,也就是有很多的 View,那么当一个事件发生时,如何来确定是哪个 View 响应了这个事件呢,接下来我们就一起来看看。
寻找hit-test view
什么是 hit-test view 呢?简单来说就是你触发事件所在的那个 View,寻找 hit-test view 的过程就叫做 Hit-Testing。
发生触摸事件后,系统会将该事件加入到一个由 UIApplication 管理的事件队列中,为什么是队列而不是栈呢?因为队列是先进先出,触摸的处理也是顺序执行的。
UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口 keyWindow,主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。找到合适的视图控件后,就会调用视图控件的 touches 方法来作具体的事件处理:
|
|
那么响应链是如何找到最合适的控件来处理事件的呢?
- 自己是否能接收触摸事件;
- 触摸点是否在自己身上;
- 从后往前遍历子控件数组,重复前面的两个步骤;
- 如果没有符合条件的子控件,那么就自己最适合处理;
下面是 hitTest 实现的伪代码:
|
|
在查找最合适的 View 的过程中用到了两个最重要的方法
|
|
hitTest:withEvent:
只要事件一传递给一个控件,这个控件就会调用自己的 hitTest:withEvent: 方法,用于寻找并返回最合适的 View, 它不管这个控件能不能处理事件也不管点是否在 View 上,事件都会先传给这 View 再调用这个 View 的 hitTest:withEvent: 方法。不管点击哪里,最合适的 View 都是 hitTest 返回的那个 View。
利用这个特性可以拦截事件的处理:
事件传递给谁就会调用这个 View 的
hitTest:withEvent:方法,如果返回nil,那么该方法的控件本身和子控件不是最合适的 View,那么最合适的 View 就是该控件的父控件。
如果想让 A 成为最合适的 View 就重写 A 的父控件 B 的 hitTest:withEvent: 方法,或者自己的 hitTest:withEvent: 方法返回 self,建议采用第一种。
特殊情况
谁都不能处理事件,窗口也不能处理。
重写 window 的
hitTest:withEvent:方法返回nil只能由窗口处理事件。
控制器的 view 的
hitTest:withEvent:方法返回nil或者 window 的
hitTest:withEvent:方法返回self返回
nil的含义:调用当前
hitTest:withEvent:方法return nil的意思是 View 不是合适的 View,子控件也不是合适的 View。如果同级的兄弟控件也没有合适的 View,那么最合适的 View 就是父控件。
pointInside:withEvent
pointInside:withEvent: 方法判断点在不在当前 View 上(方法调用者的坐标系上)如果返回 YES,代表点在方法调用者的坐标系上;
返回 NO 代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。我们可以重写这个方法,主动拦截事件的传递:
|
|
响应者对象(Responsder Object)
响应者对象是能够响应并且处理事件的对象,UIResponder 是所有响应者对象的父类,包括 UIApplication、UIView 和 UIViewController 都是 UIResponder 的子类。也就意味着所有的 View 和 ViewController 都是响应者对象。
第一响应者(First Responder)
第一响应者是第一个接收事件的 View 对象,我们在 Xcode 的 Interface Builder 画视图时,可以看到视图结构中就有 First Responder。

这里的 First Responder 就是 UIApplication 了。另外,我们可以控制一个 View 让其成为 First Responder,通过实现 canBecomeFirstResponder 方法并返回 YES 可以使当前 View 成为第一响应者,或者调用 View 的 becomeFirstResponder 方法也可以,例如当 UITextField 调用该方法时会弹出键盘进行输入,此时输入框控件就是第一响应者。
事件传递机制
如上所说,如果 hit-test view 不能处理当前事件,那么事件将会沿着响应者链(Responder Chain)进行传递,直到遇到能处理该事件的响应者(Responsder Object)。通过下图,我们来看看两种不同情况下得事件传递机制。

左边的情况,接收事件的 initial view 如果不能处理该事件并且她不是顶层的 View,则事件会往它的父 View 进行传递。initial view 的父 View 获取事件后如果仍不能处理,则继续往上传递,循环这个过程。如果顶层的 View 还是不能处理这个事件的话,则会将事件传递给它们的 ViewController,如果 ViewController 也不能处理,则传递给 UIWindow,此时 Window 不能处理的话就将事件传递给 UIApplication,最后如果连 Application 也不能处理,则废弃该事件。
右边图的流程唯一不同就在于,如果当前的 ViewController 是有层级关系的,那么当子 ViewController 不能处理事件时,它会将事件继续往上传递,直到传递到其 Root ViewController,后面的流程就跟之前分析的一样了。
这就是事件响应者链的传递机制,通过这些内容,我们可以更深入的了解事件在 iOS 中得传递机制,对我们在实际开发中更好的理解事件操作的原理有很大的帮助,也对我们实现复杂布局进行事件处理时增添了多一份的理解。
事件传递的完整过程
- 先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件;
- 调用最合适控件的
touches…方法; - 如果调用了
super touches…就会将事件顺着响应者链条往上传递,传递给上一个响应者; - 接着就会调用上一个响应者的
touches….方法;
判断上一个响应者
- 如果当前这个 View 是控制器的 View,那么控制器就是上一个响应者;
- 如果当前这个 View 不是控制器的 View,那么父控件就是上一个响应者;
UIResponder
如果你观察一下 UIView 的子类,可以发现 3 个基类: reponders (响应者),views (视图)和 controls (控件)。我们快速重温一下它们之间发生了什么。
UIResponder 是 UIView 的父类。responder 能够处理触摸、手势、远程控制等事件。之所以它是一个单独的类而没有合并到 UIView 中,是因为 UIResponder 有更多的子类,最明显的就是 UIApplication 和 UIViewController。通过重写 UIResponder 的方法,可以决定一个类是否可以成为第一响应者,例如当前输入焦点元素。
iOS 中要响应事件都必须继承 UIResponder,且是对象,我们称之为响应者对象。 继承 UIResponder 的有:
UIApplicationUIViewControllerUIView
当触摸或运动传感器等交互行为发生时,它们被发送给第一响应者,通常是一个视图。如果第一响应者没有处理,则该行为沿着响应链到达视图控制器,如果行为仍然没有被处理,则继续传递给应用。如果想监测晃动手势,可以根据需要在这 3 层中的任意位置处理。
UIResponder 还允许自定义输入方法,从 inputAccessoryView 向键盘添加辅助视图到使用 inputView 提供一个完全自定义的键盘,UIResponder 内部提供了以下方法来处理事件:
|
|
UIControl
UIControl 建立在视图上,增加了更多的交互支持。最重要的是,它增加了 target / action 模式。看一下具体的子类,我们可以看一下按钮,日期选择器,文本框等等。创建交互控件时,你通常想要子类化一个 UIControl。
一些常见的像 bar buttons 虽然也支持 target / action,和 text view 其实并不是 UIControl。
UIControl 是控制对象,继承于 UIView,如传达用户意图的应用程序按钮和滑块的基类。你不能使用 UIControl 的类直接实例化控件。相反,它定义了它的所有子类的通用接口和行为结构
UIControl主要包括触摸事件、加速事件、远程事件这几种。
UIControl 的常用属性
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
常用方法
|
|
|
|
|
|
|
|
|
|
|
|
|
|
UIGestureRescognizer
UIGestureRescognizer 是一类手势识别器对象,它可以附属在你指定的 View 上,并且为其设定指定的手势操作,例如是点击、滑动或者是拖拽。当触控事件发生时,设置了Gesture Recognizer`s` 的 View 会先通过识别器去拦截触控事件,如果该触控事件是事先为 View 设定的触控监听事件,那么Gesture Recognizers 将会发送动作消息给目标处理对象,目标处理对象则对这次触控事件进行处理,先看看如下流程图。

在 iOS 中,View 就是我们在屏幕上看到的各种 UI 控件,当一个触控事件发生时,Gesture Recognizer`s` 会先获取到指定的事件,然后发送 `action message` 给目标对象 `target`,目标对象就是 `ViewController`,在 `ViewController` 中通过事件方法完成对该事件的处理。Gesture Recognizers 能设置诸如单击、滑动、拖拽等事件,通过 Action-Target 这种设计模式,好处是能动态为 View 添加各种事件监听,而不用去实现一个 View 的子类去完成这些功能。
常用手势识别类
在 UIKit 框架中,系统为我们事先定义好了一些常用的手势识别器,包括点击、双指缩放、拖拽、滑动、旋转以及长按,通过这些手势识别器我们可以构造丰富的操作方式。
| 子类 | 作用 |
|---|---|
| UITapGestureRecognizer | 敲击 |
| UIPinchGestureRecognizer | 捏合,用于缩放 |
| UIPanGestureRecognizer | 拖拽 |
| UISwipeGestureRecognizer | 轻扫 |
| UIRotationGestureRecognizer | 旋转 |
| UILongPressGestureRecognizer | 长按 |
在上表中可以看到,UIKit 框架中已经提供了诸如 UITapGestureRecognizer 在内的六种手势识别器,如果你需要实现自定义的手势识别器,也可以通过继承 UIGestureRecognizer 类并重写其中的方法来完成,这里我们就不详细讨论了。
每一个 Gesture Recognizer 关联一个 View,但是一个 View 可以关联多个 Gesture Recognizer,因为一个 View可能还能响应多种触控操作方式。当一个触控事件发生时,Gesture Recognizer 接收一个动作消息要先于 View 本身,结果就是 Gesture Recognizer 作为 View 处理触控事件的代表,或者叫代理。当 Gesture Recognizer 接收到指定的事件时,它就会发送一条 action message 给 ViewController 并处理。
UIGestureRescognizer 常用属性和方法:
|
|
UILongPressGestureRecognizer 常用属性和方法
|
|
UISwipeGestureRecognizer 常用属性和方法
|
|
UIRotationGestureRecognizer 常用属性和方法
|
|
UIPanGestureRecognizer 常用属性和方法
|
|
连续和不连续动作
触控动作同时分为连续动作和不连续动作,连续动作例如滑动和拖拽,它会持续一小段时间,而不连续动作例如单击,它瞬间就会完成,在这两类事件的处理上又稍有不同。
对于不连续动作,Gesture Recognizer 只会给 ViewContoller 发送一个单一的动作消息,而对于连续动作,Gesture Recognizer 会发送多条动作消息给 ViewContoller,直到所有的事件都结束。
为一个 View 添加 GestureRecognizer 有两种方式,一种是通过 InterfaceBuilder 实现,另一种就是通过代码实现,我们看看通过代码来如何实现。
|
|
在事件处理过程中,这两种方式所处的状态又各有不同,首先,所有的触控事件最开始都是处于可用状态 Possible,对应 UIKit 里面的 UIGestureRecognizerStatePossible 类,如果是不连续动作事件,则状态只会从 Possible 转变为已识别状态 Recognized 或者是失败状态 Failed。例如一次成功的单击动作,就对应了 Possible-Recognized 这个过程。

手势识别有以下几种状态:
| 枚举值 | 定义 |
|---|---|
| UIGestureRecognizerStatePossible | 没有触摸事件发生,所有手势识别的默认状态 |
| UIGestureRecognizerStateBegan | 一个手势已经开始但尚未改变或者完成时 |
| UIGestureRecognizerStateChanged | 手势状态改变 |
| UIGestureRecognizerStateEnded | 手势完成 |
| UIGestureRecognizerStateCancelled | 手势取消,恢复至Possible状态 |
| UIGestureRecognizerStateFailed | 手势失败,恢复至Possible状态 |
如果是连续动作事件,如果事件没有失败并且连续动作的第一个动作被 Recognized,则从 Possible 状态转移到 Began 状态,这里表示连续动作的开始,接着会转变为 Changed 状态,在这个状态下会不断循环的处理连续动作,直到动作执行完成变转变为 Recognized 已识别状态,最终该动作会处于完成状态 Ended,另外,连续动作事件的处理状态会从 Changed 状态转变为 Canceled 状态,原因是识别器认为当前的动作已经不匹配当初对事件的设定了。
UITouch & UIEvent
在屏幕上的每一次动作事件都是一次 Touch,在 iOS 中用 UITouch 对象表示每一次的触控,多个 Touch 组成一次 Event,用 UIEvent 来表示一次事件对象。目前 iOS 设备支持的多点操作手指数最多是 5,下图展示了一个 UIEvent 对象与多个 UITouch 对象之间的关系。

当用户触摸屏幕后,就会产生相应的事件,所有相关的 UITouch 对象都被包装在事件中,被程序交由特定的对象来处理。UITouch 对象直接包括触摸的详细信息,比如触摸的位置、时间、阶段。
当手指移动时,系统会更新同一个 UITouch 对象,使之能够一直保存该手指在的触摸位置,当手指离开屏幕时,系统会销毁相应的 UITouch 对象。
UITouch 的常用属性和方法
|
|
当手指接触到屏幕,不管是单点触摸还是多点触摸,事件都会开始,直到用户所有的手指都离开屏幕。期间所有的 UITouch 对象都被包含在 UIEvent 事件对象中,由程序分发给处理者,事件记录了这个周期中所有触摸对象状态的变化。
只要屏幕被触摸,系统就会报若干个触摸的信息封装到 UIEvent 对象中发送给程序,由管理程序 UIApplication 对象将事件分发。一般来说,事件将被发给主窗口,然后传给第一响应者对象处理。
在上述过程中,完成了一次双指缩放的事件动作,每一次手指状态的变化都对应事件动作处理过程中得一个阶段。通过 Began-Moved-Ended 这几个阶段的 Touch 共同构成了一次事件 Event。在事件响应对象 UIResponder 中有对应的方法来分别处理这几个阶段的事件。
|
|
4 个触摸事件处理方法中,都有 NSSet *touches 和 UIEvent *event 两个参数,一次完整的触摸过程中,只会产生一个事件对象,4 个触摸方法都是同一个 event 参数。
如果两根手指同时触摸一个 View,那么 View 只会调用一次 touchesBegan:withEvent: 方法,touches 参数中装着 2 个 UITouch 对象。
如果这两根手指一前一后分开触摸同一个 View,那么 View 会分别调用 2 次 touchesBegan:withEvent: 方法,并且每次调用时的 touches 参数中只包含一个 UITouch 对象。根据 touches 中 UITouch 的个数可以判断出是单点触摸还是多点触摸。
UIEvent 常用属性和方法
|
|
UIEvent 是代表 iOS 系统中的一个事件,一个事件包含一个或多个的 UITouch;UIEvent 分为三类:
UIEventTypeTouches触摸事件,通过触摸、手势进行触发,例如手指点击、缩放;UIEventTypeMotion运动事件,通过加速器进行触发,例如手机晃动;UIEventTypeRemoteControl远程控制事件,通过其他远程设备触发,例如耳机控制按钮;
触摸对象的事件类型包括一个或多个触摸,触摸与某一事件联系在一起。一个触摸是被一个 UITouch 对象调用的。当一个事件触发了,系统将会把它传递给合适的响应对象并通过 UIEvent 对象发出一个消息。
调用 UIResponder 方法如 touchesBegan:withEvent:,响应对象可以分配触摸事件到合适的触摸类型并适当的控制他们。UIEvent 中的方法可以让你获取全部的触摸事件 allTouches 或者给定的视图或者窗口 touchesForView: 或者 touchesForWindow:,它可以分辨从响应对象传递过来的事件对象发生的时间 timestamp。
一个 UIEvent 对象贯穿在多点触摸事件的序列中,UIKit 重用同一个 UIEvent 实例来分配每一个事件到应用程序。你不需要保持一个事件对象或者任何从事件对象返回的对象。如果你需要保存事件对象然后传递到另外一个对象,你需要从 UITouch 或者 UIEvent 中复制信息。
你可以通过类型属性和子类型属性,获取事件类型和事件的子类型。UIEvent 定义了事件的类型为触摸,摇晃和遥控事件,它也定义了摇晃事件的子类型,以及为遥控事件定义了一系列的子类型。
参考链接
iOS触摸事件的流动:http://shellhue.github.io/2017/03/04/FlowOfUITouch/
iOS事件的传递与响应:http://blog.csdn.net/yongyinmg/article/details/19616527