这篇文章主要想弄清楚事件(如触摸屏幕)产生后,系统是如何通知到你的 App,在 App 内部是如何进行传递,最终又是如何肯定最终的响应者的。html
这些确定是有规则的,在 App 内部,一个事件会按照一个规则(视图层级关系)去遍历寻找这个事件的最佳响应者,可是这个响应者有可能不处理事件,那么它又须要沿着必定的规则(响应者链)去传递这个事件,若是最终都无人处理,那么将这个事件抛弃,也就是不处理。ios
先来看看什么是事件。git
事件对应的对象为 UIEvent
,它有一个属性为 type
,是 EventType
类型,EventType
是一个枚举类型:github
public enum EventType : Int {
case touches // 触摸事件
case motion // 运动事件
case remoteControl // 远程控制事件
@available(iOS 9.0, *)
case presses // 按压事件
}
复制代码
因此 iOS 中的事件有四种:面试
触摸事件就是咱们的手指或者苹果的 Pencil(触笔)在屏幕中所引起的互动,好比轻点、长按、滑动等操做,是咱们最常接触到的事件类型。触摸事件对象能够包含一个或多个触摸,而且每一个触摸由 UITouch
对象表示。当触摸事件发生时,系统会将其沿着线路传递,找到适当的响应者并调用适当的方法,例如 touchedBegan:withEvent:
。响应者对象会根据触摸来肯定适当的方法。算法
触摸事件分为如下几类:bootstrap
UILongPressGestureRecognizer
)UIPanGestureRecognizer
)UIPinchGestureRecognizer
)UIScreenEdgePanGestureRecognizer
)UISwipeGestureRecognizer
)UIRotationGestureRecognizer
)UITapGestureRecognizer
)button
相关触摸事件对应的对象为 UITouch
。swift
iPhone 内置陀螺仪、加速器和磁力仪,能够感知手机的运动状况。iOS 提供了 Core Motion
框架来处理这些运动事件。根据这些内置硬件,运动事件大体分为三类:服务器
X-Y-Z
轴的自转速率、倾斜角度等。经过 Core Motion
提供的一些 API 能够获取到这些数据,并进行处理;经过系统能够经过内置陀螺仪获取设备的朝向,以此对 App UI 作出调整X-Y-Z
轴速度的改变;Core Motion
提供了高度计(CMAltimeter
)、计步器(CMPedometer
)等对象,来获取处理这些产生的数据不过官方文档中指出,这些都是属于 Core Motion
库框架,Core Motion
库中的事件直接由 Core Motion
内部进行处理,不会经过响应者链,因此 UIKit
框架能接收的事件暂时只包括摇一摇(EventSubtype.motionShake
)。app
远程控制事件容许响应者对象从外部附件或耳机接受命令,以便它能够管理音频和视频。目前 iOS 仅提供咱们远程控制音频和视频的权限,即对音频实现暂停/播放、上一曲/下一曲、快进/快退操做。如下是它能识别的类型:
public enum EventSubtype : Int {
case remoteControlPlay
case remoteControlPause
case remoteControlStop
case remoteControlTogglePlayPause
case remoteControlNextTrack
case remoteControlPreviousTrack
case remoteControlBeginSeekingBackward
case remoteControlEndSeekingBackward
case remoteControlBeginSeekingForward
case remoteControlEndSeekingForward
}
复制代码
iOS 9.0 以后提供了 3D Touch 事件,经过使用这个功能能够作以下操做:
咱们通常说的事件传递的起点在于 UIApplication
所管理的事件队列中开始分发的时候,但事件真正的起点在于你手指触摸到屏幕的那一刻开始(以触摸事件为例),那么在触摸屏幕到事件队列开始分发发生了什么?咱们就以一个触摸事件来讲明这个过程。
IOKit.framework
将事件封装成一个 IOHIDEvent
对象mach port
(IPC 进程间通讯)转发到 Springboardmach port
(IPC 进程间通讯)转发给当前 App 的主线程RunLoop
接收到 Springboard 转发过来的消息以后,触发对应的 mach port
的 Source1
回调 __IOHIDEventSystemClientQueueCallback()
Source1
回调内部触发了 Source0
的回调 __UIApplicationHandleEventQueue()
Source0
回调内部,封装 IOHIDEvent
为 UIEvent
Source0
回调内部调用 UIApplication
的 +sendEvent:
方法,将 UIEvent
传给当前 UIWindow
IOKit.framework
是一个系统框架的集合,用来驱动一些系统事件。IOHIDEvent
中的HID
表明 Human Interface Device,即人机交互驱动
SpringBoard
是一个应用程序,用来管理 iOS 的主屏幕,除此以外像WindowServer(窗口服务器)
、bootstrapping(引导应用程序)
,以及在启动时候系统的一些初始化设置都是由这个特定的应用程序负责的。它是咱们 iOS 程序中,事件的第一个接收者。它只能接受少数的事件,好比:按键(锁屏/静音等)、触摸、加速、接近传感器等几种 Event,随后使用mach port
转发给须要的 App 进程
UIApplication
管理了一个事件队列,之因此是队列而不是栈,是由于队列的特色是先进先出,先产生的事件先处理。UIApplication
会从事件队列中取出最前面的事件,并将事件分发下去以便处理,一般,先发送事件给应用程序的主窗口(keyWindow
),主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个处理过程的第一步。
流程图(图1):
UIWindow
接收到的事件,有的是经过响应者链传递,找到合适的响应者进行处理;有的不须要传递,直接用 first responder
来处理。这里咱们主要说须要沿着响应者链传递的过程。
事件的传递大体能够分为三个阶段:
经过手或触笔触摸屏幕所产生的事件,都是经过这三步去传递的,如前面提到的触摸事件和按压事件。
其实这是肯定第一响应者的过程,第一响应者也就是做为首先响应这次事件的对象。对于每次事件发生以后,系统会去找能处理这个事件的第一响应者。根据不一样的事件类型,第一响应者也不一样:
view
UIKit
指定的那个对象UIKit
指定的那个对象UIKit
指定的那个对象与加速计、陀螺仪、磁力仪相关的运动事件,是不遵循响应链机制传递的。Core Motion 会将事件直接传递给你所指定的第一响应者。
当点击一个 view
,事件传递到 UIWindow
后,会去遍历 view
层级,直到找到合适的响应者来处理事件,这个过程也叫作 Hit-Test。
既然是遍历,就会有必定的顺序。系统会根据添加 view
的先后顺序,肯定 view
在 subviews
中的顺序,而后根据这个顺序将视图层级转化为图层树,针对这个树,使用倒序、深度遍历的算法,进行遍历。之因此要倒叙,是由于最顶层的 view
最有可能成为响应者。
Hit-Test 在代码中对应的方法为:
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
// hitTest 内部调用下面这个方法
func point(inside point: CGPoint, with event: UIEvent?) -> Bool
复制代码
详细步骤:
keyWindow
接收到 UIApplication
传递过来的事件,首先判断本身可否接受触摸事件,若是能,那么判断触摸点在不在本身身上keyWindow
身上,那么 keyWindow
会从后往前遍历本身的子控件(为了寻找最合适的 view
)view
,若是没有更合适的子控件,那么本身就是最合适的 view
每当手指接触屏幕,UIApplication
接收到手指的事件以后,就会去调用 UIWindow
的 hitTest:withEvent:
,看看当前点击的点是否是在 window
内,若是是则继续依次调用 subView
的 hitTest:withEvent:
方法,直到找到最后须要的 view
。调用结束而且 hit-test view
肯定以后,这个 view
和 view
上面依附的手势,都会和一个 UITouch
的对象关联起来,这个 UITouch
会做为事件传递的参数之一,咱们能够看到 UITouch
的头文件中有一个 view
和 gestureRecognizers
的属性,就是 hit-test view
和它的手势。
以下图(图2):
Hit-Test 是采用递归的方法从 view
层级的根节点开始遍历,来经过一个例子看一下它是如何工做的(图3):
UIWindow
有一个 MainView
,MainView
里面有三个 subView
:viewA
、viewB
、viewC
。它们各自有两个 subView
,它们的层级关系是:viewA
在最下面,viewB
在中间,viewC
最上(也就是 addSubview
的顺序,越晚 add
进去越在上面),其中 viewA
和 viewB
有一部分重叠。
若是手指在 viewB.1
和 viewA.2
重叠的方面点击,按照上面的递归方式,顺序以下图所示(图4):
当点击图中位置时,会从 viewC
开始遍历,先判断点在不在 viewC
上,不在。转向 viewB
,点在 viewB
上。转向 viewB.2
,判断点在不在 viewB.2
上,不在。转向 viewB.1
,点在 viewB.1
上,且 viewB.1
没有子视图了,那么 viewB.1
就是最合适的 view
。遍历到这里也就结束了。
来看一下 hitTest:withEvent:
的实现原理,UIWindow
拿到事件以后,会先将事件传递给图层树中距离最靠近 UIWindow
那一层最后一个 view
,而后调用其 hitTest:withEvent:
方法。注意这里是先将视图传递给 view
,再调用其 hitTest:withEvent:
方法,并遵循如下原则:
point
不在这个视图内,则去遍历其余视图point
在这个视图内,可是这个视图还有子视图,那么将事件传递给子视图,而且调用子视图的 hitTest:withEvent:
point
在这个视图内,而且这个视图没有子视图,那么 return self
,即它就是那个最合适的视图point
在这个视图内,而且这个视图没有子视图,可是不想做为处理事件的 view
,那么能够 return nil
,事件由父视图处理另外, UIView
有些状况下是不能接受触摸事件的:
userInteractionEnabled = NO
alpha < 0.01
,会直接影响子控件的透明度。alpha
在 0 到 0.01 之间会被当成透明处理注:若是父控件不能接受触摸事件,那么子控件就不可能接受到事件。
综上,咱们能够得出 hitTest:withEvent:
方法的大体实现以下:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 是否能响应 touch 事件
if !isUserInteractionEnabled || isHidden || alpha <= 0.01 { return nil }
if self.point(inside: point, with: event) { // 点击是否在 view 内
for subView in subviews.reversed() {
// 转坐标
let convertdPoint = subView.convert(point, from: self)
// 递归调用,直到有返回值,不然返回 nil
let hitTestView = subView.hitTest(convertdPoint, with: event)
if hitTestView != nil {
return hitTestView!
}
}
return self
}
return nil
}
复制代码
用一张图来表示 hitTest:withEvent:
的调用过程(图是 OC 语法)(图5):
肯定了最合适的 view
,接下来就是识别是何种事件,在触摸事件中,对应的就是何种手势。Gesture Recognizer
(手势识别器)是系统封装的一些类,用来识别一系列常见的手势,例如点击、长按等。在上一步中肯定了合适的 view
以后,UIWindow
会将 touches
事件先传递给 Gesture Recognizer
,再传递给视图。能够自定义一个手势验证一下。
Gesture Recognizer 拥有的状态以下:
public enum State : Int {
// 还没有识别是何种手势操做(但可能已经触发了触摸事件),默认状态
case possible
// 手势已经开始,此时已经被识别,可是这个过程当中可能发生变化,手势操做还没有完成
case began
// 手势状态发生改变
case changed
// 手势识别完成(此时已经松开手指)
case ended
// 手势被取消,恢复到默认状态
case cancelled
// 手势识别失败,恢复到默认状态
case failed
// 手势识别完成,同 end
public static var recognized: UIGestureRecognizer.State { get }
}
复制代码
Gesture Recognizer 有一套本身的 touches
方法和状态转换机制。一个手势老是以 possible
状态开始,代表它已经准备好开始处理事件。从该状态开始,开始识别各类手势,直到它们到达 ended
、cancelled
或 failed
状态。手势识别器会保持在其中的一个最终状态,直到当前事件序列结束,此时 UIKit 重置手势识别器并将其返回 possible
状态。
再来看看触摸事件的类型:
UILongPressGestureRecognizer
)UIPanGestureRecognizer
)UIPinchGestureRecognizer
)UIScreenEdgePanGestureRecognizer
)UISwipeGestureRecognizer
)UIRotationGestureRecognizer
)UITapGestureRecognizer
)苹果将手势识别器分为两种大类型,一个是离散型手势识别器(Discrete Gesture Recognizer),一个是连续型手势识别器(Continuous Gesture Recognizer)。离散型手势一旦识别就没法取消,并且只会调用一次操做事件,而连续型手势会屡次调用操做事件,而且能够取消。在以上手势中,只有点击手势(UITapGestureRecognizer
)属于离散型手势。
离散型手势识别示意图(图6):
连续型手势识别的状态转换通常可分为三个阶段:
began
或 failed
状态changed
或 cancelled
状态ended
状态以下图(图7):
识别出手势以后,就要肯定由谁来响应这个事件了,最有机会处理事件的对象就是经过 Hit-Test 找到的视图或者第一响应者,若是两个都不能处理,就须要传递给下一位响应者,而后依次传递,该过程与 Hit-Test 过程正好相反。Hit-Test 过程是从上向下(从父视图到子视图)遍历,touch
事件处理传递是从下向上(从子视图到父视图)传递。下一位响应者是由响应者链决定的,那咱们先来看看什么是响应者链。
Response Chain,响应链,通常咱们称之为响应者链。在咱们的 app 中,全部的视图都是按照必定的结构组织起来的,即树状层次结构,每一个 view
都有本身的 superView
,包括 controller
的 topmost view
(即 controller
的 self.view
)。当一个 view
被 add
到 superView
上的时候,它的 nextResponder
属性就会被指向它的 superView
。当 controller
被初始化的时候,self.view
(topmost view
) 的 nextResponder
会被指向所在的 controller
,而 controller
的 nextResponder
会被指向 self.view
的 superView
,这样,整个 app 就经过 nextResponder
串成了一条链,这就是咱们所说的响应者链。因此响应者链式一条虚拟的链,并无一个对象来专门存储这样的一条链,而是经过 UIResponder
的属性串联起来的。
响应者链示意图(图8):
即(右图):
initial view
)尝试处理事件,若是不能处理,则将事件传递给其父视图(superView1
)superView1
尝试处理事件,若是不能处理,传递给它所属的视图控制器(viewController1
)viewController1
尝试处理事件,若是不能处理,传递给 superView1
的父视图(superView2
)superView2
尝试处理事件,若是不能处理,传递给 superView2
所属的视图控制器(viewController2
)viewController2
尝试处理事件,若是不能处理,传递给 UIWindow
UIWindow
尝试处理事件,若是不能处理,传递给 UIApplication
UIApplication
尝试处理事件,若是不能处理,抛弃该事件再附一个苹果官方的图(图9):
在 iOS 中,只有继承于 UIResponder
的对象、或者它自己才能成为响应者。不少常见的对象均可以相应事件,好比 UIApplication
、UIViewController
、全部的 UIView
(包括 UIWindow
)。
UIResponder
提供了如下方法来处理事件:
// 触摸事件
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
@available(iOS 9.1, *)
open func touchesEstimatedPropertiesUpdated(_ touches: Set<UITouch>)
// 运动事件
open func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionCancelled(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
// 远程控制事件
open func remoteControlReceived(with event: UIEvent?)
// 按压事件
open func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesChanged(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?)
复制代码
提供如下属性和方法来管理响应链:
// 负责事件传递,默认返回 nil,子类必须实现此方法。
open var next: UIResponder? { get }
// 判断是否能够成为第一响应者
open var canBecomeFirstResponder: Bool { get } // default is NO
// 将对象设置为第一响应者
open func becomeFirstResponder() -> Bool // default is NO
// 判断是否能够放弃第一响应者
open var canResignFirstResponder: Bool { get } // default is YES
// 放弃对象的第一响应者身份
open func resignFirstResponder() -> Bool // default is YES
// 判断对象是否为第一响应者
open var isFirstResponder: Bool { get }
复制代码
补充一下 next
:UIResponder
类并不自动保存或设置下一个响应者,该方法的默认实现是返回 nil
。子类的实现必须重写这个方法来设置下一响应者。UIView
的实现是返回管理它的 UIViewController
对象(若是它有)或其父视图;UIViewController
的实现是返回它的视图(self.view
)的父视图;UIWindow
的实现是返回 UIApplication
另外说一下 UITouch
,对于触摸事件(对应的对象为 UITouch
),系统提供了四个方法来处理:
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
@available(iOS 9.1, *)
open func touchesEstimatedPropertiesUpdated(_ touches: Set<UITouch>)
复制代码
解释一下 touchesEstimatedPropertiesUpdated(_ touches: Set<UITouch>)
,当没法获取真实的 touches 时,UIKit 会提供一个预估值,并设置到 UITouch 对应的 estimatedProperties 中监测更新。当收到新的属性更新时,会经过调用此方法来传递这些更新值。当使用 Apple Pencil 靠近屏幕边缘时,传感器没法感应到准确的值,此时会获取一个预估值赋给 estimatedProperties 属性。不断去更新数据,直到获取到准确的值。
上面的前四个方法,是由系统自动调用的:
view
只接收到一个 UITouch
对象。当你使用多个手指同时触摸时,会接收多个 UITouch
对象,每一个手指对应一个。多个手指分开触摸,会调用屡次 touches
系列方法,每一个 touches
里面有一个 UITouch
对象super.touchesxxx
方法,不然事件处理就中断于此,不会继续传递来看一下 UITouch
对象,它保存了事件的相关信息:
// 触摸事件产生或变化的时间,单位是秒
open var timestamp: TimeInterval { get }
// 当前触摸事件所处的状态
open var phase: UITouch.Phase { get }
// 短期内点按屏幕的次数
open var tapCount: Int { get }
// 触摸产生时所处的视图
open var view: UIView? { get }
// 触摸产生时所处的窗口
open var window: UIWindow? { get }
// 依附在 view 上的手势
open var gestureRecognizers: [UIGestureRecognizer]? { get }
// 使用硬件设备点击时,以点为圆心的 touch 半径,以此肯定 touch 范围的大小
open var majorRadius: CGFloat { get }
// 半径公差
open var majorRadiusTolerance: CGFloat { get }
// 一些方法
/** 返回值表示触摸点在 view 上的位置 调用时传入的 view 参数为 nil 的话,返回的是触摸点在 UIWindow 的位置 */
open func location(in view: UIView?) -> CGPoint
// 记录了前一个触摸点的位置
open func previousLocation(in view: UIView?) -> CGPoint
复制代码
以几个例子来讲明事件传递与响应在项目中的运用,其实运用主要是围绕 hitTest:withEvent:
和 pointInside:
的使用,这里简单举个例子。
touch
区域在实际开发中,有些 button
面积很小,不容易点击上。这时候你想扩大 button
的响应区域,能够经过重写 hitTest:withEvent:
方法实现,以下图的状况(图10):
实现代码:
class MyButton: UIButton {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !isUserInteractionEnabled || isHidden || alpha <= 0.01 { return nil }
let inset : CGFloat = 45 - 78
let touchRect = bounds.insetBy(dx: inset, dy: inset)
if (touchRect.contains(point)) {
for subView in subviews.reversed() {
let convertdPoint = subView.convert(point, from: self)
let hitTestView = subView.hitTest(convertdPoint, with: event)
if hitTestView != nil {
return hitTestView!
}
}
return self
}
return nil
}
}
复制代码
或者直接改 pointIndside
方法:
class MyButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return bounds.insetBy(dx: 45-78, dy: 45-78).contains(point)
}
}
复制代码
以前没作过摇一摇,感受还挺好玩的,就放在这里,其实很简单。
import UIKit
class ShakeView : UIView {
override var canBecomeFirstResponder: Bool { // 记得重写这个方法
return true
}
override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
print("摇一摇")
}
}
override func motionCancelled(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
print("取消")
}
}
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
print("结束")
}
}
}
class ViewController: UIViewController {
lazy var shakeView : ShakeView? = {
let shakeView = ShakeView(frame: view.bounds)
shakeView.backgroundColor = #colorLiteral(red: 0.08779912442, green: 0.6471169591, blue: 0.9447124004, alpha: 1)
return shakeView
}()
override func viewDidLoad() {
super.viewDidLoad()
// 设置支持摇一摇
UIApplication.shared.applicationSupportsShakeToEdit = true
view.addSubview(shakeView!)
shakeView?.becomeFirstResponder()
}
}
复制代码
来个总结吧。
iOS 中的事件:
事件从产生到系统传递到 App 的 keyWindow
:
IOKit.framework
将事件封装成一个 IOHIDEvent
对象mach port
(IPC 进程间通讯)转发到 Springboardmach port
(IPC 进程间通讯)转发给当前 App 的主线程RunLoop
接收到 Springboard 转发过来的消息以后,触发对应的 mach port
的 Source1
回调 __IOHIDEventSystemClientQueueCallback()
Source1
回调内部触发了 Source0
的回调 __UIApplicationHandleEventQueue()
Source0
回调内部,封装 IOHIDEvent
为 UIEvent
Source0
回调内部调用 UIApplication
的 +sendEvent:
方法,将 UIEvent
传给当前 UIWindow
事件传递分为三步:
view
,即第一响应者)touch
事件)1.Hit-Test:
keyWindow
接收到 UIApplication
传递过来的事件,首先判断本身可否接受触摸事件,若是能,那么判断触摸点在不在本身身上keyWindow
身上,那么 keyWindow
会倒序遍历本身的子控件view
,若是没有,那么本身就是最合适的 view
能够看看图2。
2.Gesture Recognizer:
UIWindow
会首先将 touches
事件传递给 Gesture Recognizer
,再传递给视图。
触摸事件的具体类型有:
UILongPressGestureRecognizer
)UIPanGestureRecognizer
)UIPinchGestureRecognizer
)UIScreenEdgePanGestureRecognizer
)UISwipeGestureRecognizer
)UIRotationGestureRecognizer
)UITapGestureRecognizer
)苹果又将手势识别器分为两大类型,离散型和连续型,上述类型中只有点击手势(UITapGestureRecognizer
)属于离散型。
手势识别器拥有的状态:
public enum State : Int {
// 还没有识别是何种手势操做(但可能已经触发了触摸事件),默认状态
case possible
// 手势已经开始,此时已经被识别,可是这个过程当中可能发生变化,手势操做还没有完成
case began
// 手势状态发生改变
case changed
// 手势识别完成(此时已经松开手指)
case ended
// 手势被取消,恢复到默认状态
case cancelled
// 手势识别失败,恢复到默认状态
case failed
// 手势识别完成,同 end
public static var recognized: UIGestureRecognizer.State { get }
}
复制代码
3.Response Chain
事件沿着响应链传递,传递顺序与寻找第一响应者的顺序正好相反。
传递顺序:
initial view
)尝试处理事件,若是不能处理,则将事件传递给其父视图(superView1
)superView1
尝试处理事件,若是不能处理,传递给它所属的视图控制器(viewController1
)viewController1
尝试处理事件,若是不能处理,传递给 superView1
的父视图(superView2
)superView2
尝试处理事件,若是不能处理,传递给 superView2
所属的视图控制器(viewController2
)viewController2
尝试处理事件,若是不能处理,传递给 UIWindow
UIWindow
尝试处理事件,若是不能处理,传递给 UIApplication
UIApplication
尝试处理事件,若是不能处理,抛弃该事件官方文档 About the Gesture Recognizer State Machine
官方文档 Implementing a Discrete Gesture Recognizer
官方文档 Implementing a Continuous Gesture Recognizer
官方文档 Using Responders and the Responder Chain to Handle Events