在iOS中,事件(Events)是APP接受用户输入的一种方式。在iOS开发中比较重要的事件包括点击事件(Touch Events),运动事件(Motion Events)以及远程控制事件(Remote Control Events)。然而,在一篇文章里把它们一个个都详尽的讨论完并不是一件理智的事,苹果也有官方的说明,所以我们今天的主角只是其中最重要的点击事件,并以此为例,一起来讨论一下iOS的事件系统。
目录:
- 响应器 (
UIResponder
) - iOS事件的生命周期
- hitTest
- 响应链(Responder Chain)
- 总结
响应器 (UIResponder
)
在iOS中,用来处理事件的抽象类是UIResponder
,它包含了处理事件所需的常用方法,比如我们比较熟悉的
- (BOOL)canBecomeFirstResponder
- (void)becomeFirstResponder
- (BOOL)canResignFirstResponder
- (void)resignFirstResponder
等等。它是整个iOS事件系统的核心,UIView
, UIViewController
甚至UIApplication
都是它的子类。可以说,响应器这个类是iOS事件系统的一个规范和协议,除非有特殊需要,开发者都应该按照这个协议处理事件,这样才能保证其他人看你的代码时不至于懵圈。
iOS事件的生命周期
iOS接收事件是从UIApplication
开始的。熟悉Runloop
的朋友应该知道,在主队列上运行的Runloop
其中的一个步骤便是检测事件。当Runloop
检测到事件时,便会依照一个称为hitTest
的过程将事件转移到相应的类去进行处理。通过hitTest
找到对本次事件负责的UIResponder
类实例之后,事件便会通过Responder Chain
依次传送,直至被某个UIResponder
截断或者传回UIApplication
。一个事件的生命周期大概就是这样一个过程。下面我们来分别详细讨论一下生命周期的两个阶段:hitTest
和Responder Chain 事件处理
。
hitTest
hitTest的主要目的是确定哪个UIResponder
应该对事件负责。当UIApplication接收到一个单击事件时,它会调用UIWindow的hitTest:withEvent:
方法确定负责的UIResponder
实例。而确定是否对事件负责的标准就是看点击的点是否在它的frame范围内,这个可以通过调用pointInside:withEvent:
来检测。如果确定了这个点在UIWindow的frame里面,它将会依次递归地调用它的rootview和subviews等的hitTest:withEvent
方法,直到找到最前面的能处理这个事件的UIResponder
,然后返回。
看个例子:(这里借用苹果官方的一张图解释一下)
如图,假设这样一个结构。当用户点击D的中心时,A首先接收到'hitTest:withEvent:'消息。由于点击的点在A的范围内,A变回依次问B和C。由于点不在B的范围内,B便会返回nil,于是A继续给C发hitTest:withEvent:
消息。因为C包含了点击的点,于是C给D发hitTest:withEvent:
的消息。这时候由于D没有subview,于是直接返回D自己。这样D就成了这个时间的第一响应人(firstResponder)。
这里要注意两点:
- 这是一个深度优先的遍历过程(DFS Traversal);
- 当A收到
hitTest:withEvent:
的消息时,先给B还是C发的顺序,笔者并没有找到相关的官方文档对此说明。但测试的结果显示,是按照- (NSArray *)subviews
返回结果的逆顺序。
hitTest讨论
- 当
UIView
的clipsToBounds
设置为NO
时,有可能出现以下的布局:(B是A的子视图)
这是如果点击的是B超出A的位置,那么当A收到hitTest:withEvent:
信息时,由于该点不在A的frame内,A便会返回nil了,而不会再向B发送消息询问。这样即使用户想点击的是B,B也无法接收到该事件。
当然,如果一定要对此进行处理,也可以用重载视图A的hitTest:withEvent:
方法,让其给子视图B发送消息,从而实现B能收到相应事件的需求。
- 当需要截获处理某一事件,并且阻止subviews收到该事件时,可自行重载当前UIView中的
hitTest:withEvent:
方法,如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (/* 判断事件event为要截获的事件 */) {
return self;
}
return [super hitTest:point withEvent:event];
}
响应链(Responder Chain)
在hitTest步骤完成之后将会得到一个该事件的第一负责人,一般称之为该事件的hit-test view,由于一般都是一个UIView实例。然后,该hit-test view的- (BOOL)canBecomeFirstResponder
方法将会被调用。(注意:UIView默认返回值是NO,需要在子类中重载这个方法返回YES)
如果hit-test view表示无法响应该事件,则该事件将会依照一个所谓的响应链(Responder Chain)依次往上层传递,直至有实例响应并处理该事件或一直回到UIApplication。确定传递对象的方法是调用自身的- (UIResponder *)nextResponder
方法,开发者可以利用这个来debug。
系统默认的响应链构成如下:
- hitTest返回的hit-test view将会在响应链的最前面;
- hit-test view的superview将会是它的nextResponder;
- 依次沿着superview的路径传递下去,知道遇到一个
UIViewController
的view
,然后它的nextResponder
将会是这个UIViewController
实例。 - 以此类推,直到一个
UIViewController
是UIWindow
的根控制器,这样这个UIViewController
的nextResponder
将会是UIWindow
; -
UIWindow
的nextResponder
就会是UIApplication
- 最后的一个响应器是
UIApplicationDelegate
。
当然,这些都是可以被修改的。但除非你非常确定需要修改,否则不要去更改这些默认的设定,因为那样很容易让其他人看不懂你的代码。
总结
了解这些流程可以让你在开发过程中少踩很多坑。很多iOS新手开发最容易碰到的棘手问题就是不知道为什么我的touch事件明明加上去了却并没有得到相应,希望看了这篇文章之后,你就可以自己去分析为什么了。其实我写这篇文章的本意是还想总结一些常见的错误,但不知不觉啰里啰嗦也写了这么长了,只有下一篇再进行总结了。读者如果对自己踩过的相关的坑记忆犹新的话,也可以在评论里进行回复。笔者将尽力在下一篇文章中进行详尽的分析,希望通过这种方式把经验分享给更多的人。