iOS中的响应者链

目录

介绍


响应者链(Responder Chain)是iOS开发中非常重要的一个概念,了解它可以让你对iOS中事件传递有更深刻的理解,从而编写出更具有想象力的代码。

概念与原理


这里会尽量通俗的讲解几个关键名词的基本概念,此外还会比较细致探究寻找第一响应者的过程。

响应者

在了解响应者链之前,我们先要理解什么是「响应者」。简单来说,响应者就是一个能响应(处理)事件的「实例对象」。在iOS中,只要是UIResponder的子类的实例对象便是响应者。常见的UIResponder子类有以下几种:

  • UIView
  • UIViewController
  • UIApplication

响应者链

了解了响应者之后, 也就不难理解响应者链了。正如其名,它实际上是由响应者组成的一个链表。下图中(UIButton->UIView->UIView->UIViewController->UIWindow->UIApplication->UIApplicationDelegate)就是其中一条响应者链。

第一响应者

第一响应者:最适合处理这个事件的响应者,你可以认为它就是事件苦苦追寻的「如意郎君」,同时也是响应者链的起始点(表头)。如果第一响应者不去处理相应的事件话,它就会将事件传递给响应者链中下一个结点。

下一响应者(nextResponder)

UIResponder有一个nextResponder属性(swift中是next),指向其下一个响应者结点,也可以理解为数据结构链表中的next指针。nextResponder属性是只读的,所以在运行时不能动态的修改它,但是你可以自定义UIResponder的子类重写(override )它的get方法。

UIKit中的一些类就已经重写了该属性的get方法

描述
UIView 若该View是UIViewController根视图,则nextResponder指向UIViewController的实例对象;否则,nextResponder指向该View的父视图
UIViewController 若该视图控制器是Window的根视图控制器,则nextResponder指向Window;若由另外一个视图控制器呈现,则nextResponder指向这另外一个视图控制器。
UIWindow nextResponder指向UIApplication的实例对象。
UIApplication nextResponder指向AppDelegate(去项目里看看AppDelegate文件,是不是继承UIResponder)

寻找第一响应者

如何确定第一响应者?Apple官方文档给出了相应的列表:

事件类型 第一响应者
触摸事件(Touch events) 所触摸的那个视图
按压事件(Press events) 获得焦点的那个视图
摇晃运动事件(Shake-motion events) 由开发者或UIKit指定
远程控制事件(Remote-control events) 由开发者或UIKit指定
编辑菜单消息(Editing menu messages) 由开发者或UIKit指定

从上表中可以看到,我们更加关心如何确定触摸事件的第一响应者。

寻找触摸事件的第一响应者

首先要来UIView理解两个函数:

函数名 说明
hitTest:withEvent: 在该视图树中寻找最适合作为第一响应者的View
pointInside:withEvent: 返回某个point是否在当前View所覆盖的范围中。

这里给出一份网上流传比较广的hitTest大概逻辑代码,与真实代码肯定会有一定出入,仅供参考。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

总结一下上面代码的逻辑

  1. (isUserInteractionEnabled属性设置为NO) 或 (视图被隐藏)或(透明度<=0.01的视图)直接返回nil。这个不难理解,满足以上条件的视图不被当成事件的接收者,在这里我称它为「障碍响应者」。
  2. 当传入的Point不在该视图范围内时,返回nil。这个也很好理解,当Point不在该视图范围内,也就没有必要去递归hitTest其子视图了。
  3. 当Point在该视图内且该视图不是「障碍响应者」时,倒序递归调用子视图的hitTest。这里要注意几点:第一,倒序遍历是因为视图树上后加入的视图的z轴坐标值更高,优先让最贴近屏幕的那个视图作为响应者。第二,调用子视图的hitTest时,会将当前Point转换成子视图坐标系的Point。第三,当遇到第一个合适的视图(z轴值高的那个)时直接返回。

下面给出一个例子,来理解hitTest的过程。

这个界面所对应的视图树为:

当我们点击黑色块时hitTest的函数调用顺序如下:

  • 0号粉色块
  • 3号黑色块(最终确认3号为最合适的响应者)

当我们点Button时,hitTest的函数调用顺序如下:

  • 0号粉色块
  • 3号黑色块
  • 2号绿色块
  • 1号蓝色块
  • 4号红色块
  • Button (最终确认Button为最合适的响应者)

可以看得出,这个本质上就是对视图树进行深度优先遍历搜索,找出合适的响应者(同层结点优先遍历后加入的,也就是z轴值高的那个)。

触摸事件的传递

在确定第一响应者后,系统就可以从RunLoop开始给该响应者发送touch事件了。比较典型的是touchesMoved方法。当你抚摸某个View的时候,会先通过hitTest确定第一响应者,然后持续不断的调用该响应者的touchesMoved方法传递touch数据。

默认情况下事件会从第一响应者开始传递到响应者链的尾巴(一般是AppleDelegate)。所以如果某个响应者是该响应者链上的一个结点的话,可以通过重写touchesBegan/touchesEnded/touchesMoved... 方法来截取事件。

下面有两种情况比较特殊,需要注意:

  1. 在重写touchesBegan/...的代码中没调用super.touchesBegan/...的话,就不会继续将事件传递下一个响应者了。
  2. UIControl的实例对象默认不会将事件传递给下一个响应者。但是如果你把它的target设置为nil,它会在响应者链上寻找匹配的action。

关于第2点,这里还是给出一个例子帮助理解:
ViewController的代码:

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIButton *button;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //这里target设置为nil
    [_button addTarget:nil action:@selector(tap) forControlEvents:UIControlEventTouchUpInside];
}
@end

AppDelegate的代码:

#import "AppDelegate.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

- (void)tap {
    NSLog(@"here"); // 由于之前的按钮的target设置为nil,UIKit会通过响应者链找到这里,并调用这个这个方法。

}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
}
- (void)applicationWillTerminate:(UIApplication *)application {
}

@end

参考文章


Using Responders and the Responder Chain to Handle Events

发表评论

电子邮件地址不会被公开。 必填项已用*标注