事件传递:响应者链
当你设计一个app的时候,你很可能需要你的app能够动态响应某些事件。比如,触摸可以发生在屏幕上不同对象上,你需要决定哪些对象来响应一个特定的事件,并了解对象是如何接收事件。
当一个用户事件产生的时候,UIKit
创建一个事件对象,这个对象包含了如何处理这个事件的信息。然后将这个事件对象放到这个活跃app的事件队列中。对于触摸事件,这些对象都到包裹成了UIEvent
对象。对于运动事件,取决于你使用的框架和你感兴趣的运动事件的类型。
一个事件沿着一个特定的路径,知道它被传递到一个能处理这个事件的对象。首先,事件最先被UIApplication
单例对象获取,这个对象还负责这个对象的分发。典型的,这事件被传递到app的主窗口对象,也即使把时间传递给一个初始对象来处理。初始对象取决于事件的类型。
- 触摸事件
对于触摸事件,主窗口对象首先尝试把事件传递给触摸发生的视图对象上。这个对象是一个hit-test
视图对象。找到hit-test
视图对象的过程叫hit-testing
。下面会详细说明: - 运动事件和远程事件
对于这两类事件,主窗口对象把事件发送给第一响应者。
最终的目标是找到一个对象能处理这个事件和响应这个事件。因此,UIkit对象首先把事件发送给最适合处理这个事件的对象。对于触摸事件,这个对象是hit-test
视图,对于其他的事件,这个对象是第一响应者。接下来的部分会详细说明hit-test
视图和 第一响应者是如何被决定的。
Hit-Testing 返回触摸发生的视图
iOS使用hit-testing
来寻找被触摸的视图。Hit-testing
包括检查一个触摸是否发生在相关的视图对象上。如果是,就递归的去检查这个视图的所有子视图。最底层视图包含了触摸点的视图成为hit-test
视图。在知道hit-test
视图之后,事件交给这个视图来处理。
说明:假设用户触摸了视图E在上图中。iOS通过以下检查子视图的顺序来找到hit-test
视图:
- 1.触摸事件发生在视图A的边界内。所以检测它的子视图B和C。
- 2.触摸没有发生在B,但是发生在C内。所以检测C的子视图D和E。
- 3.触摸没有发生在D,但是发生在E内。
视图E是包含触摸点堆栈视图上最底层的视图。所以视图E成为hit-test
视图。
方法 hitTest:withEvent:
根据一个给定的CGPoint
和UIEvent
返回指定的hit-test视图。hitTest:withEvent:
方法首先调用pointInside:withEvent:
开始,如果传递到hitTest:withEvent:
的点在这个视图的bounds内,那么pointInside:withEvent:
返回YES
。继而,在返回YES
的所有子view上递归调用hitTest:withEvent:
。
如果传递hitTest:withEvent:
的点不在视图的边界内,第一次调用pointInside:withEvent:
方法会返回NO
,这个点会被忽略,hitTest:withEvent:
方法返回nil
.如果一个子视图返回NO
。那么这个子视图的所有堆栈上的视图全部被忽略。因为触摸没有发生在这个子视图上,就更不可能发生在其子视图的子视图上了。这意味着如果一个子视图超出了其父视图的边界,则超出边界的部分是不会响应事件。这部分父视图都无法接收时间,更不用说往下传递了。如果子视图的clipsToBounds
属性被设置为NO
。
注意:一个触摸事件在其生命周期内,跟这个
hit-test
视图绑定,即便这个触摸事件滑出了当前视图。
这个hit-test
视图被给予第一优先权处理这个触摸事件。如果这个视图不能处理这个事件,这个触摸事件顺着响应者链向上传递,直到找到第一个能处理这个事件的对象。
响应者链是由一系列响应者连成的链
很多类型的事件的传递都依赖于响应者链。响应者链是一系列连接在一起的响应者对象。它从第一响应者开始,以application
对象结束。如果第一响应者不能处理这个事件,它会把这个事件沿着这个响应者链传递到下一个响应者。
一个响应者对象是一个能响应并能处理事件的对象。UIResponder
类是所有响应者的父类,它定义了事件处理和常见响应者行为的通用编程接口。UIApplication
,UIViewController
以及UIView
类的实例对象都是响应者,这表明,所有视图和绝大多数主控制器都是响应者。需要注意的是核心动画的图层对象不是响应者。
第一响应者被指定为首先接收事件的对象。通常,第一响应者是一个视图对象。一个对象要成为第一响应者,需要满足下面两个条件:
- 1.重写
canBecomeFirstResponder
方法并返回YES
。 - 2.接收
becomeFirstResponder
消息。如果有必要,一个对象可以给自己发送这个消息。
注意:在指定一个对象为第一响应者之前,要确保这个对象在其合适的生命周期内。比如,通常在重写的
viewDidAppear:
方法中调用becomeFirstResponder
方法。如果你试图在viewWillAppear:
给一个第一响应者赋值,由于对象还没有创建完成。那么becomeFirstResponder
方法会返回NO
。
事件并不是唯一依赖于响应者链的对象。响应者链在以下处理中都会用到:
-
触摸事件
如果hit-test
视图不能处理这个触摸事件,则从这个视图开始顺着响应者链向上级传递。 -
运动事件
UIKit
框架的对象如果要处理摇晃事件,第一响应者必须要实现UIResponder
类的motionBegin:withEvent:
或motionEnded:withEvent:
方法。 -
远程事件
如果要处理远程事件,第一响应者比如要实现UIResponder
类的remoteControlReceivedWithEvent:
方法。 -
Action消息
当用户在操纵一个控件时,例如一个按钮或者一个开关,这个action
方法的target
为空,这个消息会顺着响应者链从第一响应者(可能是这个控件自身)往后传递。 -
快捷菜单消息
当用户点击了快捷菜单的菜单时,iOS使用响应者链来找到一个实现了必要方法的对象来处理。(实现cut:
,copy:
,paste:
) -
文本编辑
当用户点击一个textField
或者textView
,则这个文本控件自动变成第一响应者。默认的响应是弹出虚拟键盘,这个文本控件获得焦点,可以开始编辑。你还可以根据应用的需要自定义键盘。你还可以给任何响应者对象添加自定义的输入控件。
UIKit
自动将用户点击的文本控件设置为第一响应者,其他对象,则需要显示的调用becomeFirstResponder
方法来成为第一响应者。
响应者链的路径
如果初始对象,hit-test
视图或者第一响应者不能处理事件。UIKit
把事件传递给响应者链上的下一个响应者。每一个响应者决定是否处理这个事件或者是把这个事件继续传递给下一个响应者,使用nextResponder
方法。直到找到一个响应者或再也没有别的响应者。一个响应者序列,从iOS检测到事件并传递给初始对象开始,通常是一个视图。这个初始视图有第一优先权来处理这个事件。下图显示了两种事件传递路径对于不同的应用配置。一个应用的事件传递路径由其具体的结构决定,但遵循着同样的规律。
对于左图,事件传递路线
- 1.
initial view
试图处理这个事件或消息。如果它不能处理这个事件,它把事件传递给它的父视图,因为initial view
不是控制器的根视图。 - 2.父视图试图处理这个事件。如果它也不能处理这个事件,则继续传递给它的父视图,因为当前视图还不是控制器的根视图。
- 3.控制器的根视图试图处理这个事件,如果根视图还不能处理这个事件,则把事件传递给它的控制器。
- 4.控制器视图处理这个事件,如果不能,则把事件传递给窗口对象。
- 5.如果窗口对象还不能处理它,则把事件传递给单例的
application
对象。 - 6.如果单例的
application
对象也不能处理,则事件被忽略。
右边的情形有点稍微的不同,但都遵循着以下规律:
- 1.事件向上传递,直到遇到某个控制器的根视图。
- 2.根视图把事件传递给它的控制器。
- 3.控制器接着把事件传递给包含它的视图,一直传递到另一层根视图。1~3步重复,直到找到根控制器。
- 4.根控制器把事件传递给窗口对象。
- 5.窗口对象把事件传递给
application
对象。
重要:如果你实现一个自定义视图来处理远程事件,
action
消息,UIKit
运动事件,快捷菜单消息。不要直接把事件或者消息传递给下一个响应者。取而代之的是,你应该先调用父类的实现,让UIKit
为你处理响应者链的遍历。