iOS概念攻坚之路(六):事件传递与响应

前言

这篇文章主要想弄清楚事件(如触摸屏幕)产生后,系统是如何通知到你的 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 中的事件有四种:面试

  • touch events(触摸事件)
  • motion events(运动事件)
  • remote-control events(远程控制事件)
  • press events(按压事件)

1.触摸事件

触摸事件就是咱们的手指或者苹果的 Pencil(触笔)在屏幕中所引起的互动,好比轻点、长按、滑动等操做,是咱们最常接触到的事件类型。触摸事件对象能够包含一个或多个触摸,而且每一个触摸由 UITouch 对象表示。当触摸事件发生时,系统会将其沿着线路传递,找到适当的响应者并调用适当的方法,例如 touchedBegan:withEvent:。响应者对象会根据触摸来肯定适当的方法。算法

触摸事件分为如下几类:bootstrap

  • 手势事件
    • 长按手势(UILongPressGestureRecognizer
    • 拖动手势(UIPanGestureRecognizer
    • 捏合手势(UIPinchGestureRecognizer
    • 响应屏幕边缘手势(UIScreenEdgePanGestureRecognizer
    • 轻扫手势(UISwipeGestureRecognizer
    • 旋转手势(UIRotationGestureRecognizer
    • 点击手势(UITapGestureRecognizer
  • 自定义手势
  • 点击 button 相关

触摸事件对应的对象为 UITouchswift

2.运动事件

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

3.远程控制事件

远程控制事件容许响应者对象从外部附件或耳机接受命令,以便它能够管理音频和视频。目前 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
}
复制代码

4.按压事件

iOS 9.0 以后提供了 3D Touch 事件,经过使用这个功能能够作以下操做:

  • Quick Actions:重压 App icon 能够进行不少快捷操做
  • Peek and Pop:使用这个功能对文件进行预览和其余操做,能够在手机自带 “信息” 里面试验
  • Pressure Sensitivity:压力响应敏感,能够在备忘录中选择画笔,按压不一样力度画出来的颜色深浅不同

事件传递到 App 以前

咱们通常说的事件传递的起点在于 UIApplication 所管理的事件队列中开始分发的时候,但事件真正的起点在于你手指触摸到屏幕的那一刻开始(以触摸事件为例),那么在触摸屏幕到事件队列开始分发发生了什么?咱们就以一个触摸事件来讲明这个过程。

  1. 手指触摸屏幕,IOKit.framework 将事件封装成一个 IOHIDEvent 对象
  2. 将这个对象经过 mach port(IPC 进程间通讯)转发到 Springboard
  3. Springboard 经过 mach port(IPC 进程间通讯)转发给当前 App 的主线程
  4. 前台 App 主线程的 RunLoop 接收到 Springboard 转发过来的消息以后,触发对应的 mach portSource1 回调 __IOHIDEventSystemClientQueueCallback()
  5. Source1 回调内部触发了 Source0 的回调 __UIApplicationHandleEventQueue()
  6. Source0 回调内部,封装 IOHIDEventUIEvent
  7. 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 来处理。这里咱们主要说须要沿着响应者链传递的过程。

事件的传递大体能够分为三个阶段:

  1. Hit-Test(寻找合适的 view)
  2. Gesture Recognizer(手势识别)
  3. Response Chain(响应链,touch 事件传递)

经过手或触笔触摸屏幕所产生的事件,都是经过这三步去传递的,如前面提到的触摸事件和按压事件。

1.Hit-Test(寻找合适的 view)

其实这是肯定第一响应者的过程,第一响应者也就是做为首先响应这次事件的对象。对于每次事件发生以后,系统会去找能处理这个事件的第一响应者。根据不一样的事件类型,第一响应者也不一样:

  • 触摸事件:被触摸的那个 view
  • 按压事件:被聚焦按压的那个对象
  • 摇晃事件:用户或者 UIKit 指定的那个对象
  • 远程事件:用户或者 UIKit 指定的那个对象
  • 菜单编辑事件:用户或者 UIKit 指定的那个对象

与加速计、陀螺仪、磁力仪相关的运动事件,是不遵循响应链机制传递的。Core Motion 会将事件直接传递给你所指定的第一响应者。

原理

当点击一个 view,事件传递到 UIWindow 后,会去遍历 view 层级,直到找到合适的响应者来处理事件,这个过程也叫作 Hit-Test。

既然是遍历,就会有必定的顺序。系统会根据添加 view 的先后顺序,肯定 viewsubviews 中的顺序,而后根据这个顺序将视图层级转化为图层树,针对这个树,使用倒序、深度遍历的算法,进行遍历。之因此要倒叙,是由于最顶层的 view 最有可能成为响应者。

Hit-Test 在代码中对应的方法为:

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
// hitTest 内部调用下面这个方法
func point(inside point: CGPoint, with event: UIEvent?) -> Bool
复制代码

详细步骤:

  1. keyWindow 接收到 UIApplication 传递过来的事件,首先判断本身可否接受触摸事件,若是能,那么判断触摸点在不在本身身上
  2. 若是触摸点在 keyWindow 身上,那么 keyWindow 会从后往前遍历本身的子控件(为了寻找最合适的 view
  3. 遍历的每个子控件都会重复上面的两个步骤(1.判断子控件是否能接受事件;2.触摸点在不在子控件上)
  4. 如此循环遍历子控件,直到找到最合适的 view,若是没有更合适的子控件,那么本身就是最合适的 view

每当手指接触屏幕,UIApplication 接收到手指的事件以后,就会去调用 UIWindowhitTest:withEvent:,看看当前点击的点是否是在 window 内,若是是则继续依次调用 subViewhitTest:withEvent: 方法,直到找到最后须要的 view。调用结束而且 hit-test view 肯定以后,这个 viewview 上面依附的手势,都会和一个 UITouch 的对象关联起来,这个 UITouch 会做为事件传递的参数之一,咱们能够看到 UITouch 的头文件中有一个 viewgestureRecognizers 的属性,就是 hit-test view 和它的手势。

以下图(图2):

Hit-Test 是采用递归的方法从 view 层级的根节点开始遍历,来经过一个例子看一下它是如何工做的(图3):

UIWindow 有一个 MainViewMainView 里面有三个 subViewviewAviewBviewC。它们各自有两个 subView,它们的层级关系是:viewA 在最下面,viewB 在中间,viewC 最上(也就是 addSubview 的顺序,越晚 add 进去越在上面),其中 viewAviewB 有一部分重叠。

若是手指在 viewB.1viewA.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):

2.Gesture Recognizer(手势识别)

肯定了最合适的 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 状态开始,代表它已经准备好开始处理事件。从该状态开始,开始识别各类手势,直到它们到达 endedcancelledfailed 状态。手势识别器会保持在其中的一个最终状态,直到当前事件序列结束,此时 UIKit 重置手势识别器并将其返回 possible 状态。

再来看看触摸事件的类型:

  • 长按手势(UILongPressGestureRecognizer
  • 拖动手势(UIPanGestureRecognizer
  • 捏合手势(UIPinchGestureRecognizer
  • 响应屏幕边缘手势(UIScreenEdgePanGestureRecognizer
  • 轻扫手势(UISwipeGestureRecognizer
  • 旋转手势(UIRotationGestureRecognizer
  • 点击手势(UITapGestureRecognizer

苹果将手势识别器分为两种大类型,一个是离散型手势识别器(Discrete Gesture Recognizer),一个是连续型手势识别器(Continuous Gesture Recognizer)。离散型手势一旦识别就没法取消,并且只会调用一次操做事件,而连续型手势会屡次调用操做事件,而且能够取消。在以上手势中,只有点击手势(UITapGestureRecognizer)属于离散型手势。

离散型手势识别示意图(图6):

连续型手势识别的状态转换通常可分为三个阶段:

  1. 初始事件序列将手势识别器移动到 beganfailed 状态
  2. 后续事件将手势识别器移动到 changedcancelled 状态
  3. 最终事件将手势识别器移动到 ended 状态

以下图(图7):

Response Chain(响应链、touch 事件传递)

识别出手势以后,就要肯定由谁来响应这个事件了,最有机会处理事件的对象就是经过 Hit-Test 找到的视图或者第一响应者,若是两个都不能处理,就须要传递给下一位响应者,而后依次传递,该过程与 Hit-Test 过程正好相反。Hit-Test 过程是从上向下(从父视图到子视图)遍历,touch 事件处理传递是从下向上(从子视图到父视图)传递。下一位响应者是由响应者链决定的,那咱们先来看看什么是响应者链。

Response Chain,响应链,通常咱们称之为响应者链。在咱们的 app 中,全部的视图都是按照必定的结构组织起来的,即树状层次结构,每一个 view 都有本身的 superView,包括 controllertopmost view(即 controllerself.view)。当一个 viewaddsuperView 上的时候,它的 nextResponder 属性就会被指向它的 superView。当 controller 被初始化的时候,self.view(topmost view) 的 nextResponder 会被指向所在的 controller,而 controllernextResponder 会被指向 self.viewsuperView,这样,整个 app 就经过 nextResponder 串成了一条链,这就是咱们所说的响应者链。因此响应者链式一条虚拟的链,并无一个对象来专门存储这样的一条链,而是经过 UIResponder 的属性串联起来的。

响应者链示意图(图8):

即(右图):

  1. 初始视图(initial view)尝试处理事件,若是不能处理,则将事件传递给其父视图(superView1
  2. superView1 尝试处理事件,若是不能处理,传递给它所属的视图控制器(viewController1
  3. viewController1 尝试处理事件,若是不能处理,传递给 superView1 的父视图(superView2
  4. superView2 尝试处理事件,若是不能处理,传递给 superView2 所属的视图控制器(viewController2
  5. viewController2 尝试处理事件,若是不能处理,传递给 UIWindow
  6. UIWindow 尝试处理事件,若是不能处理,传递给 UIApplication
  7. UIApplication 尝试处理事件,若是不能处理,抛弃该事件

再附一个苹果官方的图(图9):

UIResponder(响应者)

在 iOS 中,只有继承于 UIResponder 的对象、或者它自己才能成为响应者。不少常见的对象均可以相应事件,好比 UIApplicationUIViewController、全部的 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 }
复制代码

补充一下 nextUIResponder 类并不自动保存或设置下一个响应者,该方法的默认实现是返回 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: 的使用,这里简单举个例子。

1.增长视图的 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)
    }
}
复制代码

2.摇一摇事件

以前没作过摇一摇,感受还挺好玩的,就放在这里,其实很简单。

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

  1. 手指触摸屏幕,IOKit.framework 将事件封装成一个 IOHIDEvent 对象
  2. 将这个对象经过 mach port(IPC 进程间通讯)转发到 Springboard
  3. Springboard 经过 mach port(IPC 进程间通讯)转发给当前 App 的主线程
  4. 前台 App 主线程的 RunLoop 接收到 Springboard 转发过来的消息以后,触发对应的 mach portSource1 回调 __IOHIDEventSystemClientQueueCallback()
  5. Source1 回调内部触发了 Source0 的回调 __UIApplicationHandleEventQueue()
  6. Source0 回调内部,封装 IOHIDEventUIEvent
  7. Source0 回调内部调用 UIApplication+sendEvent: 方法,将 UIEvent 传给当前 UIWindow

事件传递分为三步:

  1. Hit-Test(寻找最合适的 view,即第一响应者)
  2. Gesture Recognizer(手势识别)
  3. Response Chain(响应链,传递 touch 事件)

1.Hit-Test:

  1. keyWindow 接收到 UIApplication 传递过来的事件,首先判断本身可否接受触摸事件,若是能,那么判断触摸点在不在本身身上
  2. 若是触摸点在 keyWindow 身上,那么 keyWindow 会倒序遍历本身的子控件
  3. 遍历的每个子控件都会重复上面两个操做(1.判断子控件是否能接受事件;2.触摸点在不在子控件上)
  4. 如此循环遍历子控件,直到找到最合适的 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

事件沿着响应链传递,传递顺序与寻找第一响应者的顺序正好相反。

传递顺序:

  1. 初始视图(initial view)尝试处理事件,若是不能处理,则将事件传递给其父视图(superView1
  2. superView1 尝试处理事件,若是不能处理,传递给它所属的视图控制器(viewController1
  3. viewController1 尝试处理事件,若是不能处理,传递给 superView1 的父视图(superView2
  4. superView2 尝试处理事件,若是不能处理,传递给 superView2 所属的视图控制器(viewController2
  5. viewController2 尝试处理事件,若是不能处理,传递给 UIWindow
  6. UIWindow 尝试处理事件,若是不能处理,传递给 UIApplication
  7. UIApplication 尝试处理事件,若是不能处理,抛弃该事件

参考文章

iOS 中的事件响应与处理

深刻浅出iOS事件机制

你真的了解UIGestureRecognizer吗?

官方文档 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

相关文章
相关标签/搜索