iOS学习笔记系列 - 事件系统

在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。一个事件的生命周期大概就是这样一个过程。下面我们来分别详细讨论一下生命周期的两个阶段:hitTestResponder Chain 事件处理

hitTest

hitTest的主要目的是确定哪个UIResponder应该对事件负责。当UIApplication接收到一个单击事件时,它会调用UIWindow的hitTest:withEvent:方法确定负责的UIResponder实例。而确定是否对事件负责的标准就是看点击的点是否在它的frame范围内,这个可以通过调用pointInside:withEvent:来检测。如果确定了这个点在UIWindow的frame里面,它将会依次递归地调用它的rootview和subviews等的hitTest:withEvent方法,直到找到最前面的能处理这个事件的UIResponder,然后返回。

看个例子:(这里借用苹果官方的一张图解释一下)

iOS学习笔记系列 - 事件系统_第1张图片
from: Apple Documentation

如图,假设这样一个结构。当用户点击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讨论
  • UIViewclipsToBounds设置为NO时,有可能出现以下的布局:(B是A的子视图)
iOS学习笔记系列 - 事件系统_第2张图片

这是如果点击的是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的路径传递下去,知道遇到一个UIViewControllerview,然后它的nextResponder将会是这个UIViewController实例。
  • 以此类推,直到一个UIViewControllerUIWindow的根控制器,这样这个UIViewControllernextResponder将会是UIWindow
  • UIWindownextResponder就会是UIApplication
  • 最后的一个响应器是UIApplicationDelegate

当然,这些都是可以被修改的。但除非你非常确定需要修改,否则不要去更改这些默认的设定,因为那样很容易让其他人看不懂你的代码。

总结

了解这些流程可以让你在开发过程中少踩很多坑。很多iOS新手开发最容易碰到的棘手问题就是不知道为什么我的touch事件明明加上去了却并没有得到相应,希望看了这篇文章之后,你就可以自己去分析为什么了。其实我写这篇文章的本意是还想总结一些常见的错误,但不知不觉啰里啰嗦也写了这么长了,只有下一篇再进行总结了。读者如果对自己踩过的相关的坑记忆犹新的话,也可以在评论里进行回复。笔者将尽力在下一篇文章中进行详尽的分析,希望通过这种方式把经验分享给更多的人。

你可能感兴趣的:(iOS学习笔记系列 - 事件系统)