2020-10-13

2020-10-13  本文已影响0人  张_何

官方文档

这里先给好用到的源码

#import "ResponseChainVC.h"
@interface ResponseBtnA: UIButton
@end
@implementation ResponseBtnA
@end
@interface ResponseBtnB: UIButton
@end
@implementation ResponseBtnB
@end

@interface ResponseBaseView: UIView
@property(copy,nonatomic) NSString *name;
@property(strong,nonatomic) UILabel *nameLabel;
@end

@implementation ResponseBaseView
-(UILabel *)nameLabel{
    if (!_nameLabel) {
        _nameLabel = [[UILabel alloc] init];
        _nameLabel.frame = CGRectMake(0, 0, 30, 20);
        _nameLabel.textColor = UIColor.blackColor;
        _nameLabel.font = [UIFont systemFontOfSize:20];
        [_nameLabel sizeToFit];
        _nameLabel.backgroundColor = UIColor.whiteColor;
    }
    return _nameLabel;
}
-(void)setName:(NSString *)name{
    _name = name;
    self.nameLabel.text = name;
    [self.nameLabel sizeToFit];
}
-(instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        [self addSubview:self.nameLabel];
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
        [self addGestureRecognizer:tap];
    }
    return self;
}
-(void)tapAction:(UITapGestureRecognizer *)sender{
    NSLog(@"%@ tapAction",NSStringFromClass([self class]));
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    NSLog(@"%@ touchesBegan",NSStringFromClass([self class]));
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *v = [super hitTest:point withEvent:event];
    NSLog(@"%@ hitTest %@",NSStringFromClass([self class]),v);
    return  v;
}
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    NSLog(@"%@ pointInside",NSStringFromClass([self class]));
    return CGRectContainsPoint(self.bounds, point);
}
@end

@interface ResponseViewA: ResponseBaseView
@end
@implementation ResponseViewA
@end

@interface ResponseViewB: ResponseBaseView
@end

@implementation ResponseViewB
@end

@interface ResponseViewC: ResponseBaseView
@end

@implementation ResponseViewC
@end

@interface ResponseViewD: ResponseBaseView
@end

@implementation ResponseViewD
@end

@interface ResponseViewE: ResponseBaseView
@end

@implementation ResponseViewE
@end

@interface ResponseViewF: ResponseBaseView
@end

@implementation ResponseViewF
@end

@interface ResponseChainVC ()
@end

@implementation ResponseChainVC
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.whiteColor;
//    [self testBtn];
    [self testView];
}
-(void)testView{
    ResponseViewA *a = [[ResponseViewA alloc] initWithFrame:CGRectMake(20, 100, 300, 250)];
    a.backgroundColor = UIColor.redColor;
    a.name = @"A";
    [self.view addSubview:a];

    ResponseViewB *b = [[ResponseViewB alloc] initWithFrame:CGRectMake(20, 100, 100, 100)];
    b.backgroundColor = UIColor.yellowColor;
    b.name = @"B";
    [a addSubview:b];

    ResponseViewC *c = [[ResponseViewC alloc] initWithFrame:CGRectMake(25, 270, 300, 250)];
    c.backgroundColor = UIColor.blueColor;
    c.name = @"C";
    [self.view addSubview:c];
    
    ResponseViewD *d = [[ResponseViewD alloc] initWithFrame:CGRectMake(30, -30, 100, 100)];
    d.backgroundColor = UIColor.greenColor;
    d.name = @"D";
    [c addSubview:d];
    
    ResponseViewE *e = [[ResponseViewE alloc] initWithFrame:CGRectMake(20, -30, 100, 100)];
    e.backgroundColor = UIColor.grayColor;
    e.name = @"E";
    [d addSubview:e];
    
    ResponseViewF *f = [[ResponseViewF alloc] initWithFrame:CGRectMake(20, 100, 100, 100)];
    f.backgroundColor = UIColor.purpleColor;
    f.name = @"F";
    [c addSubview:f];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
//    NSLog(@"ResponseChainVC touchesBegan");
//    NSLog(@"class = %@, nextResponder = %@",[self class],self.nextResponder);
//    NSLog(@"class = %@, nextResponder = %@",[self.view class],self.view.nextResponder);
//    for (int i = 0; i < self.view.subviews.count; i ++){
//        UIView *v = self.view.subviews[i];
//        NSLog(@" class = %@, nextResponder = %@",[v class],v.nextResponder);
//    }
   
    UIResponder *r = self.view.nextResponder;
    while (r) {
        NSLog(@"class = %@, nextResponder = %@",[r class],r);
        r = r.nextResponder;
    }
}

-(void)testBtn{
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.frame = CGRectMake(20, 100, 100, 100);
    btn.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn];
    
//    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
//    [btn addGestureRecognizer:tap];
    
    [btn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
}

-(void)btnAction:(UIButton *)sender{
    UIResponder *nextResponder = sender.nextResponder;
    while (nextResponder) {
        NSLog(@"btnAction nextResponder = %@",nextResponder);
        nextResponder = nextResponder.nextResponder;
    }
}

-(void)tapAction:(UITapGestureRecognizer *)sender{
    UIResponder *nextResponder = sender.view.nextResponder;
    while (nextResponder) {
       NSLog(@"tapAction nextResponder = %@",nextResponder);
       nextResponder = nextResponder.nextResponder;
    }
}

@end

// 此类只是用来测试push 出的vc和 present出来的vc中nextResponder的区别,其他情况都在ResponseChainVC中测试
@interface ResponseChainVCA: UIViewController
@end

@implementation ResponseChainVCA
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.whiteColor;
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.frame = CGRectMake(20, 100, 100, 100);
    btn.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn];
     [btn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
}

-(void)btnAction:(UIButton *)sender{
    Class c = NSClassFromString(@"ResponseChainVC");
    UIViewController *vc = [[c alloc] init];
    vc.modalPresentationStyle = UIModalPresentationFullScreen;
    [self presentViewController:vc animated:true completion:false];
//    [self.navigationController pushViewController:vc animated:true];
}


@end

事件

1、触屏事件(Touch Event)
2、运动事件(Motion Event)
3、远端控制事件(Remote-Control Event)

本文只针对触屏事件做介绍

事件的产生

响应者

查找具体响应者

ios 查找具体响应者的过程我们成为Hit-Test,那么什么是Hit-Test呢,我们可以把它理解为一个探测器,通过这个探测器我们可以找到并判断手指是否点击在某个视图上面,换句话说就是通过Hit-Test可以找到手指点击到的处于屏幕最前面的那个UIView。那么它是怎么找的呢?
首先我们看UIView的两个方法:

- (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

Hit-Test采用的是递归调用,从向根节点的UIWindow发送hitTest:withEvent:消息开始,这个消息返回一个UIView对象,当返回的UIView对象不为nil时结束,如果递归调用结束返回的是nil,则该事件被丢弃。返回的这个UIView对象就是找到的改事件的响应者。当向UIWindow发送hitTest:withEvent:消息时,hitTest:withEvent:里面所做的事,就是判断当前的点击位置是否在window里面,如果在则遍历window的subview然后依次对subview发送hitTest:withEvent:消息(注意这里给subview发送消息是根据当前subview的index顺序,index越大就越先被访问,也就是倒序的方式),一旦找到UIView对象,则递归结束。当然hitTest:withEvent:方法会先判读当前的view能否接收事件,UIView能接收事件的条件如下:
1、alpha > 0.01, 注意不包括等于,亲测
2、userInteractionEnabled属性为YES
3、hidden属性为NO
大致代码如下:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    if (self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.1) {
        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 nil;
}

派发

我们对ResponseChainVC中的btnAction断点调试,点击button触发断点我们通过控制台查看函数调用栈可以验证
对btnAction方法断点调试查看执行该方法时调用的函数栈

//(lldb) bt
//* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
//  ObjcCode`-[ResponseChainVC btnAction:](self=0x00007f9b7e515010, _cmd="btnAction:", sender=0x00007f9b8121d360) at ResponseChainVC.m:106:34
//   UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
//   UIKitCore`-[UIControl sendAction:to:forEvent:] + 223
//   UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 396
//   UIKitCore`-[UIControl touchesEnded:withEvent:] + 497
//   UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1359
//   UIKitCore`-[UIWindow sendEvent:] + 4501
//   UIKitCore`-[UIApplication sendEvent:] + 356
//   UIKitCore`__dispatchPreprocessedEventFromEventQueue + 7328
//   UIKitCore`__handleEventQueueInternal + 6565
//   UIKitCore`__handleHIDEventFetcherDrain + 88
//   CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
//   CoreFoundation`__CFRunLoopDoSource0 + 76
//   CoreFoundation`__CFRunLoopDoSources0 + 180
//   CoreFoundation`__CFRunLoopRun + 974
//   CoreFoundation`CFRunLoopRunSpecific + 404
//   GraphicsServices`GSEventRunModal + 139
//   UIKitCore`UIApplicationMain + 1605
//   ObjcCode`main(argc=1, argv=0x00007ffee93b2cb0) at main.m:14:16
//   libdyld.dylib`start + 1

响应链

下面通过例子阐述一下由nextResponder连起来的响应链

下面我们测试一下直接从导航控制器中push到ResponseChainVC的情况,
将ResponseChainVC的touchesBegan:withEvent:方法修改为

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    UIResponder *r = self.view.nextResponder;
    while (r) {
        NSLog(@"class = %@, nextResponder = %@",[r class],r.nextResponder);
        r = r.nextResponder;
    }
}

进入到ResponseChainVC页面,点击空白地方查看touchesBegan:withEvent:执行打印的情况如下:

class = ResponseChainVC, nextResponder = <UIViewControllerWrapperView: 0x7fc0a830aa90>
class = UIViewControllerWrapperView, nextResponder = <UINavigationTransitionView: 0x7fc0a82064f0>
class = UINavigationTransitionView, nextResponder = <UILayoutContainerView: 0x7fc0a5c09290>
class = UILayoutContainerView, nextResponder = <UINavigationController: 0x7fc0a6010a00>
class = UINavigationController, nextResponder = <UIDropShadowView: 0x7fc0a5e04400>
class = UIDropShadowView, nextResponder = <UITransitionView: 0x7fc0a5d05780>
class = UITransitionView, nextResponder = <UIWindow: 0x7fc0a5f07a00>
class = UIWindow, nextResponder = <UIWindowScene: 0x7fc0a8004ad0>
class = UIWindowScene, nextResponder = <UIApplication: 0x7fc0a5c060c0>
class = UIApplication, nextResponder = <AppDelegate: 0x6000019d0a20>
class = AppDelegate, nextResponder = (null)

下面我们测试一下直接从导航控制器中push到ResponseChainVCA,然后通过点击ResponseChainVCA中的button present到ResponseChainVC的情况
这里将ResponseChainVCA中的btnAction:方法修改为

-(void)btnAction:(UIButton *)sender{
    Class c = NSClassFromString(@"ResponseChainVC");
    UIViewController *vc = [[c alloc] init];
    vc.modalPresentationStyle = UIModalPresentationFullScreen;
    [self presentViewController:vc animated:true completion:false];
}

进入到ResponseChainVC页面,点击空白地方查看touchesBegan:withEvent:执行打印的情况如下:

class = ResponseChainVC, nextResponder = <UINavigationController: 0x7fbd41808200>
class = UINavigationController, nextResponder = <UITransitionView: 0x7fbd4142d0c0>
class = UITransitionView, nextResponder = <UIWindow: 0x7fbd4160d5b0>
class = UIWindow, nextResponder = <UIWindowScene: 0x7fbd41509190>
class = UIWindowScene, nextResponder = <UIApplication: 0x7fbd43804f10>
class = UIApplication, nextResponder = <AppDelegate: 0x600003c04f00>
class = AppDelegate, nextResponder = (null)

通过对比我们发现
1、ResponseChainVC.view 的nextResponder 是ResponseChainVC
2、不论是push还是present ResponseChainVC响应链的最终都是

class = UIWindow, nextResponder = <UIWindowScene>
class = UIWindowScene, nextResponder = <UIApplication>
class = UIApplication, nextResponder = <AppDelegate>
class = AppDelegate, nextResponder = (null)

其实对nextResponder有如下规则:
1、如果 view 是一个 view controller 的 root view,nextResponder 是这个 view controller.
2、如果 view 不是 view controller 的 root view,nextResponder 则是这个 view 的 superview
3、如果 view controller 的 view 是 window 的 root view, view controller 的 nextResponder 是这个 window
4、如果 view controller 是被其他 view controller presented调起来的,那么 view controller 的 nextResponder 就是发起调起的那个 view controller
5、window 的 nextResponder 是 UIApplication 对象. ios 11 之后新增了UIWindowScene,window的nextResponder变成了UIWindowScene,UIWindowScene的nextResponder是UIApplication对象
6、UIApplication 对象的 nextResponder 是 app delegate, 但是 app delegate 必须是 UIResponder 对象


事件拦截应用场景

扩大UIView事件响应区域
@interface UIButton (EnlargeEdge)
// 用 IBInspectable 修饰该属性后,该属性会在XIB中显示,可以在xib中设置
@property(nonatomic) IBInspectable CGFloat enlargeEdge;

-(void)setEnlargeWithTop:(CGFloat)top left:(CGFloat)left bottom:(CGFloat)bottom right:(CGFloat)right;
@end

#import "UIButton+EnlargeEdge.h"
#import <objc/runtime.h>

static char topKey;
static char bottomKey;
static char leftKey;
static char rightKey;

@implementation UIButton (EnlargeEdge)


-(void)setEnlargeEdge:(CGFloat)enlargeEdge {
    [self setEnlargeWithTop:enlargeEdge left:enlargeEdge bottom:enlargeEdge right:enlargeEdge];
}

-(CGFloat)enlargeEdge {
    return [objc_getAssociatedObject(self, &topKey) floatValue];
}

-(void)setEnlargeWithTop:(CGFloat)top left:(CGFloat)left bottom:(CGFloat)bottom right:(CGFloat)right {
    objc_setAssociatedObject(self, &topKey,   [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &bottomKey,   [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &leftKey,   [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &rightKey,   [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(CGRect)enlargedRect {
    NSNumber* topEdge    = objc_getAssociatedObject(self, &topKey);
    NSNumber* rightEdge  = objc_getAssociatedObject(self, &rightKey);
    NSNumber* bottomEdge = objc_getAssociatedObject(self, &bottomKey);
    NSNumber* leftEdge   = objc_getAssociatedObject(self, &leftKey);

    if (topEdge && rightEdge && bottomEdge && leftEdge) {
        return CGRectMake(self.bounds.origin.x    - leftEdge.floatValue,
                          self.bounds.origin.y    - topEdge.floatValue,
                          self.bounds.size.width  + leftEdge.floatValue + rightEdge.floatValue,
                          self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
    } else {
        return self.bounds;
    }
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect rect = [self enlargedRect];
    if (CGRectEqualToRect(rect, self.bounds)) {
        return [super pointInside:point withEvent:event];
    }
    return CGRectContainsPoint(rect, point) ? YES : NO;
}

@end
多级响应

注意点

参考
参考
参考
参考
参考

上一篇 下一篇

猜你喜欢

热点阅读