iOS响应链(Responder Chain)

最近在写一个图片浏览的需求,一些地方我使用了响应者来处理,顺便又去看看了官方文档,这里记录一下官方文档,并给出一些示例加深理解。

概述

App使用响应者对象接收和处理事件,响应者对象是任何UIResponder的实例。UIResponder的子类包括UIView,UIViewController,UIApplication等。响应者接收到原始事件数据,必须处理事件或者转发到另一个响应者对象。当你的App接收到一个事件时,UIKit自动引导事件到最合适的响应者对象,也叫做第一响应者。

不能处理的事件被传递到响应链中,这是App响应者对象动态配置的。在App中没有单一的响应链,UIKit定义了默认的规则关于对象如何被传递在一个响应者到另一个响应者,但是你可以重写响应者对象中适当的属性来改变这些规则。

下图是官方给出的一个默认响应链:
Default Responder Chain

App中包含一个UILable,UITextField,UIButton,以及2个backgroundView,如果UITextField不能响应事件,UIKit发送事件到UITextField的父视图(UIView)对象,随后是UIWindow的根视图(UIView)。从根视图,响应者链在事件传递到UIWindow之前,先转移到所拥有的UIViewController。如果UIWindow不能处理事件,UIKit传递事件到UIApplication对象,也可能到app delegate如果对象是UIResponder的实例并且不是响应链的一部分。

确定事件的第一响应者

事件的每个类型,UIKit指定一个第一响应者,然后最先发送事件到这个对象。第一响应者基于事件的类型而变化。

  • Touch event
    第一响应者是触摸事件产生的view
  • Press event
    第一响应者是焦点响应者。
  • Shake-motion events,Remote-control events,Editing menu messages
    第一响应者是你或者UIKit指定的对象。
注意:运动事件相关的加速度计、陀螺仪、磁强计都不属于响应者链。而是由CoreMotion传递事件给你指定的对象。Core Motion

控件直接与它相关的target对象使用action消息通信。当用户与控件交互时,控件调用target对象的action方法,换句话说,控件发送action消息到目标对象。Action消息不是事件,但是它仍然可以利用响应链。当控件的target对象为nil,UIKit从target对象和响应链走,直到找到一个对象实现了合适的action方法。

如果视图有添加手势识别器,手势识别器接收touch和press事件在视图接收事件之前。如果所有的视图的手势识别器都不能识别它们的手势,这些事件会传递到视图处理。如果视图不能处理它们,UIKit传递事件到响应链。

确定哪个响应者包含Touch事件

UIKit使用基于视图的hit-testing来确定Touch事件在哪里产生。UIKit将Touch位置与视图层级中的视图对象的边界进行了比较。UIView的hitTest:withEvent:方法在视图层级中执行,寻找最深的包含指定Touch的子视图,这个视图将成为Touch事件的第一响应者。

注意:如果Touch位置超过视图边界,hitTest:withEvent方法将忽略这个视图和它的所有子视图。结果就是,当视图的ciipsToBounds属性为NO,子视图超过视图边界也不会返回,即使它们包含发生的Touch。

UIKit不变的分配每一个Touch给包含它的视图。UIKit创建UITouch对象当touch第一次产生时,释放这个UITouch对象在touch结束时。当touch位置或者其他参数改变时,UIKit更新UITouch对象新的信息。只有包含它的视图这个属性不会改变。甚至这个touch位置移动刀初始视图的外面,这个属性也不会改变。

hitTest:withEvent

这个方法返回最远的子视图在视图层级中,这个子视图是能接收包含指定点的(包括它本身)。
这个方法遍历视图层级让每个子视图调用poiotInside:withEvent:方法确定哪个子视图应该接收这个touch事件。如果poiotInside:withEvent: 返回YES,那么子视图的层次是类似遍历,直到找到最前面的视图包含指定点的。如果一个视图不包含该点,那么其分支视图可以被忽略。很少需要自己调用这个方法。但是可以重写它去隐藏touch事件在子视图中。

这个方法忽略以下情况:

  • 视图是隐藏的 hidden = YES
  • 用户交互关闭的 userInteractionEnabled = NO
  • 透明度小于0.01的 alpha < 0.01

这个方法在确定命中的时候,不考虑视图的内容。因此,即使指定的点位于该视图内容的透明范围,仍然可以返回视图。

点在接收者的范围之外不会被命中,即使它们实际上处于接收者的子视图之内。如果当前视图的cilpsToBounds属性被设置为NO,影响了子视图超过当前视图会产生这种情况。

改变响应链

你可以改变响应链通过重写你的响应对象的nextResponder属性。当你这样做了之后,下一个响应者就是你设置的。
许多UIKit的类已经重写了这个属性然后返回了指定的对象。

  • UIView 如果视图是ViewController的根视图,下一个响应者为ViewController,否者是视图的父视图。
  • UIViewController 如果视图控制器是window的根视图下一个响应者为window对象。如果视图控制器是由另一个视图控制器推出来,那么下一个响应者为正在推出的视图控制器。
    -UIWindow 下一个响应者为UIApplication对象。
  • UIApplication 下一个响应者为app delegate,但是代理应该是UIResponder的一个实例 而不是 UIView,UIViewController或者app对象本身。
只看理论肯定是很迷茫的,下面我通过简单的一些示例代码演示部分内容。

在iOS中能够响应事件的都是UIResponder的子类对象。 UIResponder里有4个点击回调的方法。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

参数里可以看到有一个UITouch和一个UIEvent对象,分别代表点击对象和事件对象。

为了便于测试我先添加了一个UIView类别。

#import "UIView+Responder.h"

static inline void swizzling_exchangeMethod(Class class ,SEL originalSelector, SEL swizzledSelector) {
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (success) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@implementation UIView (Responder)


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        swizzling_exchangeMethod([UIView class], @selector(touchesBegan:withEvent:), @selector(ds_touchesBegan:withEvent:));
        swizzling_exchangeMethod([UIView class], @selector(touchesMoved:withEvent:), @selector(ds_touchesMoved:withEvent:));
        swizzling_exchangeMethod([UIView class], @selector(touchesEnded:withEvent:), @selector(ds_touchesEnded:withEvent:));
    });
}


#pragma mark - 

- (void)ds_touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event
{
    NSLog(@"%@ touch begin", self.class);
    UIResponder *next = [self nextResponder];
    while (next) {
        NSLog(@"%@",next.class);
        next = [next nextResponder];
    }
}

- (void)ds_touchesMoved: (NSSet *)touches withEvent: (UIEvent *)event
{
    NSLog(@"%@ touch move", self.class);
}

- (void)ds_touchesEnded: (NSSet *)touches withEvent: (UIEvent *)event
{
    NSLog(@"%@ touch end", self.class);
}

接着创建了4个继承于UIView的子View,AView的子视图为 BView、DView。BView的子视图为CView。

视图层级

首先是模拟官方的例子,我们点击CView,控制台输出如下:

寻找响应者.png

因为CView并不能响应这个事件,所以会一直往上寻找,和官方给的例子完全符合。
如果view上有手势呢?给AView添加一个单击手势。

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(aviewAction)];
    [aview addGestureRecognizer:tap];

- (void)aviewAction {
    NSLog(@"单击");
}

单击之后控制台显示:

识别到手势.png

长按后

长按后没识别到手势事件交给视图.png

可以发现,无论有没有手势都会调用begin方法,如果识别到手势,UIView自己的end方法不调用了,会执行单击事件。如果没有识别到手势,则会调用end方法,接着交给UIView自己处理。至于响应链的输出在前面是因为我写在了begin方法里,在使用正常使用场景里,我们点击完松开手了才响应事件,也就是end之后才响应,有手势就执行手势方法 忽略了end,所以说手势接收事件在视图接收事件之前。

现在来看一下系统是怎么通过hit-test找到究竟是哪一个View产生的Touch,也就是包含Touch事件。

为了模拟系统的实现,在+(void)load()方法里添加。然后写一下方法实现。

swizzling_exchangeMethod([UIView class], @selector(hitTest:withEvent:), @selector(ds_hitTest:withEvent:));
swizzling_exchangeMethod([UIView class], @selector(pointInside:withEvent:), @selector(ds_pointInside:withEvent:));
//模拟一下,系统真正的实现肯定不是这样的,毕竟事件我都没用上。。
- (UIView *)ds_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];
            //递归调用hitTest:withEvent继续判断
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                //在这里打印self.class可以看到递归返回的顺序。
                return hitTestView;
            }
        }
        //这里就是该视图没有子视图了 点在该视图中,所以直接返回本身,上面的hitTestView就是这个。
        NSLog(@"命中的view:%@",self.class);
        return self;
    }
    //不在这个视图直接返回nil
    return nil;
}

- (BOOL)ds_pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    BOOL success = CGRectContainsPoint(self.bounds, point);
    if (success) {
        NSLog(@"点在%@里",self.class);
    }else {
        NSLog(@"点不在%@里",self.class);
    }
    return success;
}

我点击了CView,控制台输出如下:

Touch事件的产生以及响应者链.png

从(1)这里可以看出会从UIWindow一层层的开始往子视图查找,直到找到一个视图,touch点还在这个视图里,但是该视图没有子视图,这个就是最深层的。
在(2)这里我也不明白为什么会调用2次,没找到相关资料。但是看名字应该是导航栏上的那些,最后命中的是UIStatusBarWindow,我感觉应该就是UIWindow后面的一层吧,但是UIWindow又不是加在它上面的,否则不会命中它。
在这里,(3)就是响应链了。命中CView后,立即调用了begin方法。

至于其他情况和其他视图的点击,我这里就不贴出来了。把上面代码拿去测试一下就行了。

实际使用

不规则图形的点击事件,或者扩大缩小点击范围,
还有像Tarbar中间那个凸起的按钮我感觉用这个也可以实现(这个我自己没试过) ,只要重写pointInside:withEvent:方法就行了。

觉得对你有帮助点个赞吧。有什么错误欢迎指出,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 在开发过程中,大家或多或少的都会碰到令人头疼的手势冲突问题,正好前两天碰到一个类似的bug,于是借着这个机会了解了...
    闫仕伟阅读 5,158评论 2 23
  • 好奇触摸事件是如何从屏幕转移到APP内的?困惑于Cell怎么突然不能点击了?纠结于如何实现这个奇葩响应需求?亦或是...
    Lotheve阅读 54,742评论 51 596
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,873评论 4 26
  • -- iOS事件全面解析 概览 iPhone的成功很大一部分得益于它多点触摸的强大功能,乔布斯让人们认识到手机其实...
    翘楚iOS9阅读 2,809评论 0 13
  • 概述 应用程序使用响应者对象来接收和处理事件,属于UIResponder类的实例对象都是响应者,常见的子类包括UI...
    渐z阅读 2,595评论 0 3