面试题iOS_CornBallast@IT·互联网

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

2017-04-27  本文已影响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

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

上一篇下一篇

猜你喜欢

热点阅读