iOS 触摸事件的传递与响应
status
category
date
summary
slug
icon
tags
password
绘制过程和事件分发都是 Android 和 iOS 中经常谈到的话题,虽然实习已经结束了,但依然记得当时刚开始学习 iOS,有人问我
UIButton
继承自哪个类,我想了想说:“这个我不太清楚,但 Android 中 Button
是继承自 TextView
,所以我觉得 UIButton
应该继承自 UILabel
……”现在我终于明白了为什么对方当时一脸懵逼,也很庆幸他没有继续问事件分发机制。所以现在我怀着感恩的心,学习一下相关知识。从触摸到事件产生
iOS 中有四种事件,包括触摸事件、运动事件、远程控制事件、按压事件,本文主要讨论最常见的触摸事件。事件描述了用户和应用的交互行为,而
UIEvent
就是记录事件的对象,它记录了事件发生的时间、类型,下面是 UIEvent
的几个属性(还有可以获取 UIWindow
和 UIView
对应的 UITouch
集合等其他属性没有列出):通过
allTouches
属性可以知道,一个 UIEvent
对象可以包含多个 UITouch
对象,即一个触摸事件可能有多次触摸,例如三指触摸等等。UITouch
详细记录了手指在屏幕上产生的交互,例如时间、位置、所在的窗口和视图等等(还有力度、类型 UITouchType
等属性没有列出):当事件产生之后,系统会寻找最合适的响应者对象来处理事件,如果找不到任何可以响应事件的对象,事件就会被废弃。只有继承自
UIResponder
的对象才能响应事件,常见的它的子类有 UIApplication
、UIView
、UIViewController
。这是它们的继承关系:
当手指触摸屏幕,系统会创建事件对象,并放入
UIApplication
管理的事件队列,队列的特点是 FIFO,即先进先出,符合先产生的事件先被处理的逻辑,到这里触摸事件就产生了。事件的传递
UIApplication
会不断地从事件队列中取出事件,并传递下去,找到最合适的响应者来处理事件。在 iOS UIView绘制(二)View Hierarchy 一文中说到,UIApplication
是通过 UIWindow
来管理所有视图的,具体关系如下图所示。所以一般来说,事件会首先传递给程序的主窗口(keyWindow
),然后由主窗口向下传递。
因为事件是从父 view 向子 view 传递的,所以如果父 view 不能接收触摸事件,那么子 view 也不能接收到这个事件。事件的传递是一个递归的过程,传递这个动作实际上指的是
hitTest
这个方法:顾名思义,
hitTest
方法可以返回最适合的响应者,point
方法会判断触摸点是否处于视图之中,寻找响应者的过程可以用以下代码来表示:需要注意的有这么几点:
- 能够接收事件需要同时满足三个条件:
- 用户可以交互(
isUserInteractionEnabled = true
) - 不被隐藏(
isHidden = false
) - 透明度不低于0.01(
alpha >= 0.01
)
- 遍历是从
subviews
的最后一个元素开始的,因为最后一个元素“Z轴最高”,发生重叠时会覆盖之前的视图,符合触摸时上层视图优先响应的原则
- 如果触摸的点不在父 view 上,那么子 view 的 hitTest 就不会调用。即使子 view 的大小超过了父 view,如果触摸发生在子 view 超出父 view 的区域内,依然不会返回子 view
到这里一个响应者返回时,事件就结束了传递,接下来就是事件的响应了。
事件的响应
事件从
UIApplication
的事件队列传递到到 UIWindow
再到具体的 UIView
,过程中经历了一系列对象,除了 UIApplication
,大致可以看做是父 view 把事件传递到子 view,最后找到了响应者,是一个自上向下的过程。但事件的响应可以看做是传递的逆过程,事件从响应者逐步传递到父 view,再一次经历之前的一系列对象依次处理,这些对象称为响应者链。在响应者链中,响应者要么处理事件,要么把事件“传递”给下一个响应者,注意这里的传递是指响应过程中的传递,不要和上面事件的传递过程混淆。

在上面这个例子中,如果我们点击输入框,并且
UITextField
没有处理事件,那么事件会被传递到响应者链中的下一个响应者,即图中的 UIView
,紧接着就是 UIViewController
的 Root View
,然后是 UIViewController
,再是 UIWindow
。如果 UIWindow
也没有处理事件,事件就会被移交给 UIApplication
,再由 UIApplicationDelegate
处理,前提是它继承自 UIResponder
。上面说的事件处理,实际上指的是重写 touches 相关的方法:
另外,上面的例子中,如果第一个响应者
UITextField
重写了这些方法,也就是处理了事件,可以选择是否重写 touches 方法,来控制事件是否传递到让下一个响应者,而下一个响应者一般是父 view:如果调用了 nextResponder 的 touches 对应方法,就达到了一个事件被多个对象处理的目的。
总结
事件从产生到处理主要经历了两个过程,第一个过程是事件的传递,这是一个从
UIApplication
到 UIWindow
及其子 view 的递归过程,同时是一个自顶向下的过程,这个过程的最后一个对象就是最适合的响应者,这个过程的结束也标志着传递过程的结束;第二个过程是事件的响应,同时这也是事件在响应者链上的传递,不过是从子 view 到 UIWindow
,再到 UIApplication
或者 UIApplicationDelegate
的一个自下向上的过程。有一点值注意的是,事件的传递实际上并不注重过程,而是注重结果,传递过程的目的就是找到最适合的响应者,
UIApplication
和 UIWindow
都只是寻找到最后那个 UIView
的工具,使用这些工具的技巧就是四大组件之间的关系,具体的入口就是 keyWindow
和 subviews
。但事件的响应是注重过程的,每个响应者是否处理和传递事件都是我们需要关心的,在这个过程中,UIView
的属性显得不那么重要,我们只关心 UIResponder
。这也解释了 UIViewController
在两个过程中角色的差异,传递时因为不是 UIView
而划水,响应时因为是 UIResponder
而成为响应者链的一部分。Loading...