美文网首页面试题iOS_CornBallast@IT·互联网
关于响应者链,也就如此。

关于响应者链,也就如此。

作者: 穿山甲救蛇精 | 来源:发表于2017-04-27 18:22 被阅读753次

    作为入门级的知识点,其实还有很多人搞不清楚怎么回事,举个例子:在一个父view加三个重叠的子视图(UIView),给每一个view添加一个tap事件,点击重叠区域,响应者毫无疑问是最上层的view,下面我们试两种情况:
    1 设置最上层的view的userInteractionEnabled属性为NO,你会发现第二层的view响应了事件。
    2 设置最上层的view的userInteractionEnabled属性为YES,去掉添加的tap事件,你会发现父view响应了事件。
    通过这个现象我们切入今天的主题:响应者链的传递规则以及我们可以实施的阴谋。

    引言

    王老汉有两个儿子,他的大儿子是个光棍,二儿子又两个儿子,大致的关系图如下:



    有一天,隔壁伍丽娟送来了一个肉包子,王老汉不舍得吃,于是问小王:“小王,你想吃吗?你不想吃我问问你哥。”,小王说:“我吃”,其实小王不是想自己吃,而是想给王大大和小明吃,于是问小明:“想吃吗?”,小明天生是个植物人,根本不鸟他爹,也从来都没鸟过,于是问王大大,王大大说:“我吃!”,突然王大大发现包子里有个小纸条,他彻底懵逼了,于是把这个有纸条的包子给了他爹也就是小王,小王也搞不定上面的暗语啊,于是又交给了他爹老王,老王微微一笑,出门去解决了这个包子的问题。
    是不是觉得一派胡言,那就对了。


    响应者链的模式

    首先,来了解两个方法:

    - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
    - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds
    

    - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;这个方法调用- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;并返回响应事件的view。
    比如如图格局,点击viewD,响应事件:

    1. UIWindow知道有点击事件之后,会首先调用自己的hitTest:方法,hitTest:这个方法会调用会调用自身的pointInside:方法,结果发现pointInside:方法返回YES,说明点击区域在UIWindow内,然后UIWindow遍历他的子视图调用hitTest:方法。
    2. self.view调用自身的hitTest:方法,hitTest:这个方法会调用会调用自身的pointInside:方法,结果发现pointInside:方法返回YES,从而确定点击在self.view的范围内,然后self.view遍历他的子视图调用hitTest:方法。
    3. ViewC调用自身的hitTest:方法,hitTest:这个方法会调用会调用自身的pointInside:方法,结果发现pointInside:方法返回YES,从而确定点击在ViewC的范围内,然后ViewC遍历他的子视图调用hitTest:方法。
    4. ViewE调用自身的hitTest:方法,hitTest:这个方法会调用会调用自身的pointInside:方法,结果发现pointInside:方法返回NO,从而确定点击不在ViewE的范围内,然后ViewE会在自己的hitTest:方法中返回nil;下一步轮到ViewD调用自身的hitTest:方法,hitTest:这个方法会调用会调用自身的pointInside:方法,结果发现pointInside:方法返回YES,从而确定点击在ViewD的范围内,然后ViewD遍历他的子视图调用hitTest:方法。
    5. viewD无子视图所有遍历终止,在方法- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;中返回viewD,view一层层向superView传递,最终确定返回viewD,也就是viewD响应事件;
      注意:如果某一层view的userInteractionEnabled设置为NO,那么方法- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;会直接返回nil,所以事件到这里也就终止了。

    实现过程可以这么理解:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        if (self.userInteractionEnabled) {
            if([self pointInside:point withEvent:event]){
                for (UIView *view in self.subviews) {
                    UIView *hitTestView = [view hitTest:point withEvent:event];
                    if(!hitTestView){
                        return view;
                    }
                }
            }
        }
        return nil;
    }
    

    既然确定了点击的对象,那么下一步就是响应事件,也就是利用刚才的反向顺序,如果viewD不能响应这个事件,那么便向上找,直到nextResponder响应这个点击事件,如果都不响应,这个点击事件便流失了。
    其实一句话,响应者链就是一个从下到上的定位过程以及从上到下的寻找过程,定位的是点击的view,寻找的是能够响应这个点击的view。

    这样一个概念相信大家都有了,那么它有什么作用呢。要不然一堆理论没有任何卵用。

    1. 这里既然提到了nextResponder,其实刚才说的view是一个具象化的概念,因为UIViewController也继承自UIResponder,那么说一个通过view找到当前属于的VC的方法:
    #import "UIView+Responder.h"
    @implementation UIView (Responder)
    -(UIViewController*)viewOnCurrentVC{
        UIResponder *responder = [self nextResponder];
        while (responder) {
            if ([responder isKindOfClass:[UIViewController class]]) {
                return (UIViewController*)responder;
            }
            responder = [responder nextResponder];
        }
        return nil;
    }
    @end
    
    1. 既然我们可以知道事件的传递过程,那么我们就能够截获这个事件,让我们看好的view去响应这个事件。比如:新手指导,镂空对应区域并响应镂空区域点击事件,点击新手指导非镂空区域提供事件接口。
    //
    //  YSModalView.h
    //  LibrarysDemo
    //
    //  Created by ys on 2017/4/26.
    //  Copyright © 2017年 ys. All rights reserved.
    //
    #import <UIKit/UIKit.h>
    @interface YSModalView : UIView
    /*
     *strokeColor 边框颜色
     *canRespond 是否可以响应事件
     *modalViewsArray 需要模态的视图数组
     *modalRectsArray 需要模态的rect数组
     */
    @property(nonatomic,strong)UIColor *strokeColor;
    @property(nonatomic,assign)BOOL canRespond;
    @property(nonatomic,copy)void(^tapModalBlock)();
    @property(nonatomic,strong)NSArray *modalViewsArray;
    @property(nonatomic,strong)NSArray<NSValue*> *modalRectsArray;
    /** 创建模态 */
    +(instancetype)YSModalViewOnSuperView:(UIView*)superView;
    /** 修改属性后刷新模态 */
    -(void)updateDisplay;
    @end
    //******************************************************
    //******************************************************
    //******************************************************
    //
    //  YSModalView.m
    //  LibrarysDemo
    //
    //  Created by ys on 2017/4/26.
    //  Copyright © 2017年 ys. All rights reserved.
    //
    #import "YSModalView.h"
    @implementation YSModalView
    /** 创建模态 */
    +(instancetype)YSModalViewOnSuperView:(UIView*)superView{
        if (!superView) {
            return nil;
        }
        YSModalView *modalView = [[YSModalView alloc] initWithFrame:superView.bounds];
        modalView.userInteractionEnabled = YES;
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:modalView action:@selector(tapModalView:)];
        [modalView addGestureRecognizer:tap];
        modalView.backgroundColor = [UIColor clearColor];
        [superView addSubview:modalView];
        return modalView;
    }
    -(void)tapModalView:(UITapGestureRecognizer*)tap{
        if (self.tapModalBlock) self.tapModalBlock();
    }
    /** 修改属性后刷新模态 */
    -(void)updateDisplay{
        [self setNeedsDisplay];
    }
    -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
        if (self.canRespond) {
            for (NSValue *viewValue in [self allRectArray]) {
                CGRect viewRect = viewValue.CGRectValue;
                viewRect = [self convertRect:viewRect toView:self];
                if (CGRectContainsPoint(viewRect, point)) {
                    return NO;
                }
            }
        }
        return YES;
    }
    -(void)drawRect:(CGRect)rect{
        UIBezierPath *backBezierPath = [UIBezierPath bezierPathWithRect:self.bounds];
        backBezierPath.usesEvenOddFillRule = YES;
        backBezierPath.lineWidth = 0;
        //
        UIBezierPath *strokePath = [UIBezierPath bezierPath];
        strokePath.lineWidth = 2;
        for (NSValue *viewValue in [self allRectArray]) {
            //按钮镂空位置
            CGRect viewRect = viewValue.CGRectValue;
            viewRect = [self convertRect:viewRect toView:self];
            UIBezierPath *viewPath = [UIBezierPath bezierPathWithRoundedRect:viewRect cornerRadius:5];
            [backBezierPath appendPath:viewPath];
            //虚线
            UIBezierPath *oneStrokePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(viewRect.origin.x - 2, viewRect.origin.y - 2, viewRect.size.width + 4, viewRect.size.height + 4) cornerRadius:5];
            [strokePath appendPath:oneStrokePath];
        }
        [[[UIColor blackColor] colorWithAlphaComponent:0.5] setFill];
        [backBezierPath fill];
        //
        CGFloat dash[] = {5,3};
        [strokePath setLineDash:dash count:2 phase:0];
        [self.strokeColor setStroke];
        [strokePath stroke];
    }
    //
    -(NSArray<NSValue*>*)allRectArray{
        NSMutableArray* allRectArray = [NSMutableArray arrayWithArray:self.modalRectsArray];
        for (UIView* view in self.modalViewsArray) {
            [allRectArray addObject:[NSValue valueWithCGRect:view.frame]];
        }
        return allRectArray;
    }
    @end
    

    其实诸如此类的应用时机还有很多,基础的的东西会也许会给你意想不到的惊喜。

    相关文章

      网友评论

      • 庞仕山:- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        if (self.userInteractionEnabled) {
        if([self pointInside:point withEvent:event]){
        for (UIView *view in self.subviews) {
        UIView *hitTestView = [view hitTest:point withEvent:event];
        if(!hitTestView){
        return view;
        }
        }
        }
        }
        return nil;
        }

        `if(!hitTestView)`
        这个判断是不是写反了
      • Pusswzy:真是爱死你了~ 终于明白了, 之前看别的文章一直不懂(规则是先遍历最后加的子视图viewC,如果点击事件在viewC中)的意思
      • 庞仕山:你举得这个例子,真的厉害,下面的内容都不用看了,既讲清了响应者链以及事件传递,又揭示了老王邻居的悲哀
      • be6ad6bfc9dc:好有道理的样子:smirk:

      本文标题:关于响应者链,也就如此。

      本文链接:https://www.haomeiwen.com/subject/tjfwzttx.html