iOS进阶:通过实际项目来学习掌握响应链

项目中的问题

在前段时间的项目中,遇到了一个与响应链相关的问题。效果图如下:


效果1.png
效果2.png

在默认状态下,最下方有五个按钮;当点击选中地图上的单车后,五个按钮会同时上移,并且导航视图也会跟着上移。如果是你你会如何去实现。
我的第一反应就是,将这些彼此有约束的按钮都放在一个自定义视图上,这样,当需要上移或者下移的时候,只需要改变这个自定义视图的frame即可。但是其实这样是有问题的。

首先,为了不让这个自定义视图遮盖住下面的地图,我将该视图的背景颜色改为clearColor。运行起来后,界面上没有问题,但是当我在自定义视图的透明区域滑动地图的时候,发现地图不会响应我的滑动事件。原因是该视图虽透明,但仍然遮盖在了地图上方,所以地图不会响应。虽然我知道原因,但是用户可不知道,当他发现下面部分不能滑动地图,就认为是bug了。。

我当时的解决办法

当时的我对于响应链的掌握处于知道是什么,却不会用的状态。由于时间紧迫,我只能采取一个“不太好”的方法。

我创建了一个管理下方视图的工具类,在工具类初始化的时候传入控制器的view,并在内部添加各种按钮。
这种方法其实和在控制器中添加一个个按钮没什么差别,现在只是将添加按钮的代码放到了工具类中,让控制器的代码能少点。

通过响应链来解决

在使用响应链之前,得知道响应链是怎么工作的。在接下来的文章中,我会先写响应链的相关知识,再写如何用这些相关知识去解决上面的问题。

什么是响应链

响应者链条:在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在iOS中响应者链的关系可以用下图表示:


响应链示意图.png

需要注意:

  • 如果当前这个view是控制器的view,那么控制器就是上一个响应者
  • 如果当前这个view不是控制器的view,那么父控件就是上一个响应者

上述来源:史上最详细的iOS之事件的传递和响应机制-原理篇

响应链是如何工作的(工作步骤)

第一步、事件的产生

  • 当点击其中一个视图的时候,系统会将该事件加入到一个由UIApplication管理的事件队列中
  • 取出队列里的最前面事件,分发处理

第二步、事件的传递

  • 获取到需要处理的事件后,顺着响应链,向下查找最合适的视图。

第三步、事件的响应

  • 找到最合适的视图后,会调用自己的touches方法处理事件。如果自身没有做处理(也就是自身没有重写touches方法),那么会逆着响应链,向上抛,直到找到能响应这个事件的视图。如果最上头的UIApplication也不能处理该事件或消息,则将其丢弃。

实例讲解

我用一个简单的实例来讲解。在控制器中,添加若干个视图。如同所示:


实例结构1.png
实例结构2.png

当我点击视图D的时候,会发生些什么事情呢。

  • 因为没有别的事件需要处理,会把这个点击事件拿出来处理。
  • 开始顺着响应链查找最合适的响应视图 这里就是D视图
  • D视图如果能响应这个事件就响应,如果不能上抛,到UIApplication后还是不能就丢弃。

这里有几个问题需要说明。

这里的响应链是怎么样的

响应链如下图所示:


结构图.png
示例响应链.png

如何通过这个链条找到最合适的视图

在说这个之间先得知道,响应链里的每一个类都是继承UIResponder,而该类中有以下两个方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

当在找寻响应视图的时候,会先调用hitTest方法,这个方法的用途是返回一个最合适的视图。
hitTest方法内部调用pointInside方法,这个方法的作用是点击的点是否在自身内部。

下面以实例说说:(因为UIApplication不好说,就以View A举例子)

  • 当点击视图D以后,会先进入View AhitTest方法来寻找最合适的视图。
  • hitTest方法内部先判断是否View A隐藏、不能触发事件或者透明度<0.01。如果是,这返回nil,说明最合适的视图不在View A内部;反之,继续往下。
  • 调用pointInside方法,判断点是否在自身内部。如果不在内部,那么同样返回nil;反之,说明在View A内部,继续往下。
  • View A的子视图中,倒着找。(也就是先找E 再找D中)

这样可以试着写出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;
}

上述代码来源:iOS事件响应链中Hit-Test View的应用

那么示例中,点击D视图后,是怎么调用方法的呢?
我重写了A-E五个视图的hitTestpointInside方法。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"进入A_View---hitTest withEvent ---");
    UIView * view = [super hitTest:point withEvent:event];
    NSLog(@"离开A_View--- hitTest withEvent ---hitTestView:%@",view);
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    NSLog(@"A_view--- pointInside withEvent ---");
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"A_view--- pointInside withEvent --- isInside:%d",isInside);
    return isInside;
}

(ps:这里只需要调用super的对应方法,就可以让重写没有影响了。这句话什么意思呢:如果只是在重写方法中打印数据,那么就不会继续往下找了。而如果调用了super的对应方法,也就是UIView的方法,就会继续往下找,就不会影响到程序了。)

当我点击D后,打印结果如下:


打印结果.png

最后找到了D视图。

找到视图后的响应是怎么回事

再找到视图后,会调用这个视图的touches方法。当这个视图有重写这些方法的时候,就说明这个视图能响应本次事件,如果这个视图不能响应,那么就会顺着响应链上抛,直到找到能响应这个事件的响应者。

举个栗子:
还是上面的示例,当我不写D视图的touches的方法,也就是D视图不能响应事件,那么会将这次响应上抛给C视图,如果我重写了C视图的touches方法后,会调用C视图的方法。

响应动图.gif

当我点击D视图后,调用的是C视图的touches,而当我点击E视图后,调用的是E视图的touches

解决开始的问题

下面就可以来通过响应链解决开头的问题了。

最好的思路

我把开头的需求简化成了如下:


简化示意图.png

如果什么都不操作,只是在tableview上放一个yellow viewyellow view上放两个按钮,那么在黄色视图上滚动tableview是没有用的。
原因是这样的:

  • yellow view视图上 上下滑动tableview,事件产生并开始处理。
  • 通过响应链查找最合适的响应视图
    • 先是控制器view,发现点击位置在其内部
    • 再是tableview,发现点击位置在其内部
    • 再是yellow view 发现点击位置在其内部
    • 最后是两个按钮,发现不在他们内部,那么找到最合适的视图为yellow view
  • 那么由yellow view来响应本次事件。

清楚过程后,只需要在进入yellow viewhitTest方法时,做一下处理,让其不是合适的响应视图即可。

具体如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event]; // 找到了最合适的视图
    if (hitView && hitView == self) { // 当找到了并且是自己本身时候,返回nil,告诉上一级tableview,最合适的视图不在我内部
        return nil;
    }
    return hitView;
}

逻辑如下:

  • 进入控制器view,发现点击位置在其内部,那么往其子视图中找
  • 发现只有tableview,并且进入发现点击位置在tableview中,说明最合适的响应视图要么是tableview,要么是tableview的子视图,在往tableview子视图中找找。
  • 发现了yellow view,发现了点击位置在yellow view中并且再找了其两个子视图按钮后发现yellow view正是这个最合适的视图。这个时候如果再不处理,系统默认会返回yellow view,那么tableview就没有机会响应了。但是返回了nil,告诉tableview,没找到最合适的,那么tableview就成了最合适的响应视图。

至于为什么要判断hitView == self呢,因为如果这次点击的是两个按钮,那么这里的hitView就是按钮了,如果仍然返回nil,那么按钮的响应也将无法触发。

最终效果:


项目解决动图.gif

另一个思路

这里还有另一个思路,那就是重写yellow viewpointInside方法,因为hitTest内部会调用pointInside来判断点击点是否是视图内部,如果不管三七二十一直接返回NO,那么这个视图将永远不会作为最合适的视图。因此,可以使用这个特性来实现效果:
判断一下点击点的位置,如果是两个按钮的位置,就返回YES,如果点击位置yellow view的黄色区域,就返回NO。

利用这个思路,还可以给小的按钮增加响应热区,给超出父视图的视图富裕响应能力等。

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

推荐阅读更多精彩内容