点我跳转原文地址

概述

iOS响应者链(Responder Chain)是支撑App界面交互的重要基础,点击、滑动、旋转、摇晃等都离不开其背后的响应者链,所以每个iOS开发人员都应该彻底掌握响应者链的响应逻辑,本文旨在通过demo测试的方式展现响应者链的具体响应过程,帮助读者彻底掌握响应者链。

Demo

你可以在这里(GitHub地址)下载本文测试的Demo源码,阅读本文的同时结合Demo程序有助于更加直观深刻的理解。

探究过程

响应者(Responder)

当我们触控手机屏幕时系统便会将这一操作封装成一个UIEvent放到事件队列里面,然后Application从事件队列取出这个事件,接着需要找到去响应这个事件的最佳视图也就是Responder, 所以开始的第一步应该是找到Responder, 那么又是如何找到的呢?那就不得不引出UIView的2个方法:
- -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
返回视图层级中能响应触控点的最深视图
- -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
返回视图是否包含指定的某个点
通过在显示视图层级中依次对视图调用这个2个方法来确认该视图是不是能响应这个点击的点,首先会调用hitTest,然后hitTest会调用pointInside,最终hitTest返回的那个view就是最终的响应者Responder, 那么问题来了,在视图层级中是如何确定该对哪个View调用呢?优先级又是什么?
为了探寻其中的逻辑,在Demo中我们构建了一个如下图所示的多重视图:

这是一个简单的控制器视图,在Controller的视图上添加了View1-View4共4个视图,View1-View4和RootView都继承自BaseView, BaseView继承自UIView; 其中 View1、View2是RootView的子视图,View3、View4是View2的子视图,他们的继承关系和父子关系图下图:

为了能观测到UIView的hitTest和pointInside调用过程,我们写个分类通过方法交换来打印调用的日志:

@implementation UIView (DandJ)
+ (void)load {Method origin = class_getInstanceMethod([UIView class], @selector(hitTest:withEvent:));Method custom = class_getInstanceMethod([UIView class], @selector(dandJ_hitTest:withEvent:));method_exchangeImplementations(origin, custom);origin = class_getInstanceMethod([UIView class], @selector(pointInside:withEvent:));custom = class_getInstanceMethod([UIView class], @selector(dandJ_pointInside:withEvent:));method_exchangeImplementations(origin, custom);
}- (UIView *)dandJ_hitTest:(CGPoint)point withEvent:(UIEvent *)event {NSLog(@"%@ hitTest", NSStringFromClass([self class]));UIView *result = [self dandJ_hitTest:point withEvent:event];NSLog(@"%@ hitTest return: %@", NSStringFromClass([self class]), NSStringFromClass([result class]));return result;
}- (BOOL)dandJ_pointInside:(CGPoint)point withEvent:(UIEvent *)event {NSLog(@"%@ pointInside", NSStringFromClass([self class]));BOOL result = [self dandJ_pointInside:point withEvent:event];NSLog(@"%@ pointInside return: %@", NSStringFromClass([self class]), result ? @"YES":@"NO");return result;
}@end

当我们点击视图中的View3(紫色)时看看日志输出:

从日志中我们可以看到,首先是从UIWindow开始调用hitTest, 然后经过一段导航控制器的视图,因为我们的控制器是在导航控制的,所以可以先忽略这一段,然后来到RootView,调用RootView的hitTest和pointInside,因为点击发生在RootView中所以继续遍历它的子视图,可以看到是从View2开始的,调用View2的hitTest和pointInside,pointInside返回YES,然后继续遍历View2的子视图,从View4开始,因为点击不发生在View4所以pointInside返回NO,而View4没有子视图了,所以返回了nil也就是打印出来的null,然后继续在View2的另外一个子视图View3(目标视图)中调用hitTest和pointInside,因为我们点击的就是View3所以pointInside返回YES,且View3没有子视图所以hitTest返回了自己View3,接着View2的hitTest也返回View3直到UIWindow返回View3, 自此我们找到了响应视图:View3!另外我们看到对其他的Window也有调用,只不过返回了nil。
- 结论:
1. 寻找事件的最佳响应视图是通过对视图调用hitTest和pointInside完成的
2. hitTest的调用顺序是从UIWindow开始,对视图的每个子视图依次调用,子视图的调用顺序是从后面往前面,也可以说是从显示最上面到最下面
3. 遍历直到找到响应视图,然后逐级返回最终到UIWindow返回此视图
PS:
1.关于最后一个能响应的子视图demo中是因为没有子视图而确定的,这不是唯一确定的条件,因为有些情况下视图可能会被忽略,不会调用hitTest,这与userInteractionEnabled, alpha, frame等有关,在下个demo会演示。
2.与加速度器、陀螺仪、磁力仪相关的运动事件不遵循此响应链,他们是由Core Motion 直接派发的

处理者

在上面我们已经找到了点击事件的响应者View3,但是我们并未给View3添加相应的点击处理逻辑(UITapGestureRecognizer),所以View3并不会处理事件,那么View3不处理由会交给谁处理呢?如果View3处理了又是怎么样的呢?
能够处理UI事件都是继承UIResponder的子类对象,UIResponder主要有以下4个方法来处理事件:

// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (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;

分别是对应从触摸事件的开始、移动、结束、取消,如果你想自定义响应事件可以重写这几个方法来实现。如果某个Responder没处理事件,事件会被传递,UIResponder都有一个nextResponder属性,此属性会返回在Responder Chain中的下一个事件处理者,如果每个Responder都不处理事件,那么事件将会被丢弃。所以继承自UIResponder的子类便会构成一条响应者链,所以我们可以打印下以View3为开始的响应者链是什么样的:

- (void)viewDidAppear:(BOOL)animated {[super viewDidAppear:animated];UIResponder *nextResponder = self.view3.nextResponder;NSMutableString *pre = [NSMutableString stringWithString:@"--"];NSLog(@"View3");while (nextResponder) {NSLog(@"%@%@", pre, NSStringFromClass([nextResponder class]));[pre appendString:@"--"];nextResponder = nextResponder.nextResponder;}
}

可以看到响应者链一直延伸到AppDelegate, View3的下一个是View2也就是View3的父视图,View2下一个是RootView也是父视图,而RootView的下一个则是Controller, 所以下一个响应者的规则是如果有父视图则nextResponder指向父视图,如果是控制器根视图则指向控制器,控制器如果在导航控制器中则指向导航控制器的相关显示视图最后指向导航控制器,如果是根控制器则指向UIWindow,UIWindow的nexResponder指向UIApplication最后指向AppDelegate,而他们实现这一套指向都是靠重写nextReponder实现的。

为了验证点击上面的事件的处理顺序,我们继续上面那个demo,为RootView和View1-View4的基类BaseView重写这几个方法:

@implementation BaseView- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {NSLog(@"%@ touchesBegan", NSStringFromClass([self class]));[super touchesBegan:touches withEvent:event];
}- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {NSLog(@"%@ touchesMoved", NSStringFromClass([self class]));[super touchesMoved:touches withEvent:event];
}- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {NSLog(@"%@ touchesEnded", NSStringFromClass([self class]));[super touchesEnded:touches withEvent:event];
}@end

同样也为控制器(FindResponderController)添加相关touches方法,日志打印看调用顺序:

可以看到先是由UIWindow通过hitTest返回所找到的最合适的响应者View3, 接着执行了View3的touchesBegan,然后是通过nextResponder依次是View2、RootView、FindResponderController,可以看到完全是按照nextResponder链条的调用顺序,touchesEnded也是同样的顺序。

PS:感兴趣的可以继续重写AppDelegate的相关touches方法,验证最终是不是会被顺序调用。

上面是View3不处理点击事件的情况,接下来我们为View3添加一个点击事件处理,看看又会是什么样的调用过程:

@implementation View3
- (void)awakeFromNib {[super awakeFromNib];[self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)]];
}- (void)tapAction:(UITapGestureRecognizer *)recognizer {NSLog(@"View3 taped");
}@end

运行程序,点击View3看看日志打印:

可以看到touchesBegan顺着nextResponder链条调用了,但是View3处理了事件,去执行了相关是事件处理方法,而touchesEnded并没有得到调用。

  • 总结
    1.找到最适合的响应视图后事件会从此视图开始沿着响应链nextResponder传递,直到找到处理事件的视图,如果没有处理的事件会被丢弃。
    2.如果视图有父视图则nextResponder指向父视图,如果是根视图则指向控制器,最终指向AppDelegate, 他们都是通过重写nextResponder来实现。

无法响应的情况

在[响应者]章节我们已经提到寻找最佳响应者是通过hitTest函数调用完成的,那么存在哪些情况下视图会被忽视,而不被调用hiTest呢?
下面我么也通过第2个demo来演示,在什么情况下hitTest不会被调用或者返回nil,在demo中从上到下我们分别模拟了Alpha=0、子视图超出父视图的情况、userInteractionEnabled=NO、hidden=YES这4中情况:

  • 结论
    1.Alpha=0、子视图超出父视图的情况、userInteractionEnabled=NO、hidden=YES视图会被忽略,不会调用hitTest
    2.父视图被忽略后其所有子视图也会被忽略,所以View3上的button不会有点击反应
    3.出现视图无法响应的情况,可以考虑上诉情况来排查问题

应用示例

  • 点击透传
    RootView有2个重叠在一起的子视图View1和View2, View2覆盖在View1上面,如何做到点击View1触发View2的处理逻辑?
    很简单,设置View2的userInteractionEnabled=NO即可。
  • 限定点击区域
    给定一个显示为圆形的视图,实现只有在点击区域在圆形里面才视为有效。
    我们可以重写View的pointInside方法来判断点击的点是否在圆内,也就是判断点击的点到圆心的距离是否小于等于半径就可以。
@implementation CircleView
- (void)awakeFromNib {[super awakeFromNib];self.layer.cornerRadius = self.frame.size.width / 2.0f;
}- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {const CGFloat radius = self.frame.size.width / 2.0f;CGFloat xOffset = point.x - radius;CGFloat yOffset = point.y - radius;CGFloat distance = sqrt(xOffset * xOffset + yOffset * yOffset);return distance <= radius;
}
@end

iOS响应者链彻底掌握相关推荐

  1. 分割view窗口不响应onmousewheel_什么是响应者链?ios面试攻克篇(三)

    '写在前面的话' 这些是我对iOS面试时会碰到的问题的解决方法, 整理出来分享给大家,有些错误不要 太好笑,希望对大家有所帮助. 大家有更好的解决办法也欢迎沟通交流. 一直以来都有写点儿东西的想法, ...

  2. [置顶] Responder一点也不神秘————iOS用户响应者链完全剖析

    这篇文章想跟大家分享的主旨是iOS捕获用户事件的各种情况,以及内部封装的一些特殊事件. 我们先从UIButton谈起,UIButton大家使用的太多了,他特殊的地方就在于其内置的普通Default/高 ...

  3. iOS底层原理探究 第一探. 事件传递和响应者链

    一. 声明:  本文意在探讨, 也参考了几位大神的文章, 在最后我会把链接发出来, 如果有理解错误的地方, 请大神们指正哈! 二. 前言:  最近自己做项目的时候, 用到了UITabbarContro ...

  4. iOS之深入解析响应者链Responder Chain

    一.响应链事件 iOS 中的事件可分为:触摸事件(multitouch events).加速计事件(accelerometer events).远程控制事件(remote control events ...

  5. iOS 之 事件响应者链

    响应者链表示一系列的响应者对象.事件被交由第一个响应者对象处理,如果第一个响应者不处理,事件就沿着响应者链向上传递,交由下一个响应者(Next responder). View->ViewCon ...

  6. iOS响应链和传递机制

    iOS中加载的时候会先执行main函数 int main(int argc, charchar * argv[]) { @autoreleasepool { return UIApplicationM ...

  7. Cocoa Touch事件处理流程--响应者链

    一.事件分类 对于IOS设备用户来说,他们操作设备的方式主要有三种:触摸屏幕.晃动设备.通过遥控设施控制设备.对应的事件类型有以下三种: 1.触屏事件(Touch Event) 2.运动事件(Moti ...

  8. [翻译练习] #selector() 和响应者链

    译自:swiftandpainless.com/selector-an- 由于 Swift 2.2 中 Selector 的新语法,我在 "Utilize the responder cha ...

  9. iOS生态链寄生数百万开发者:艰难求生面临决择

    k歌之王: 对当前智能机应用开发局势的较全面点评, 不过在windows phone8发布以后, 不知道那些准备转向wp的团队会作何打算~~~ 短短4年间,依托iOS系统平台,形成了一个庞大的苹果商业 ...

最新文章

  1. golang 警告提示 Error string should not be capitalized or end with punctuation mark
  2. C++学习笔记7[指针]
  3. 1095 解码PAT准考证 (25 分)
  4. 用友php漏洞,用友CRM注入漏洞(无需登录通杀所有版本)
  5. rsync和inotify实时同步配置 exclude排除多个文件夹
  6. 使用Android自带DownloadManager下载文件
  7. Property “pageNumber“ was accessed during render but is not defined on instance.
  8. 安装配置fcitx输入法
  9. 事业单位计算机岗位考公基吗,【事业单位】江苏统考考什么?公基、言语……题量分布!...
  10. Xilinx 7系列FPGA DDR3硬件设计规则
  11. 小胜凭智,大胜靠德|北京
  12. K_MEANS 聚类
  13. cocos2dx实例开发之经典三消
  14. 使用Nginx中遇到的一个小问题思考
  15. WPS word文档插入图片显示不全
  16. arcgis10.2绘制矢量数据面polygon
  17. Flink的“前世今生”
  18. 新形势下,企业如何做好数据安全治理?
  19. MySQL在Linux上的四种安装方式
  20. GPS卫星信号及播发状态

热门文章

  1. 16核处理器的服务器什么型号,16核处理器服务器
  2. 新手机安装好sim卡显示无服务器,为什么手机突然显示无sim卡_突然检测不到sim卡的处理办法...
  3. MySQL(六):数据库的导入和导出+索引+用户权限管理
  4. Apache搭建代理服务器
  5. DGL官方教程--API--dgl.DGLGraph
  6. JavaEE 企业级分布式高级架构师(四)SpringMVC学习笔记(4)
  7. 英雄联盟总结之概述(笔记分享)
  8. linux内核同步问题
  9. 重装系统出现了计算机意外的重新启动或遇到错误.
  10. 00后开始找工作了,可能挣得比你还多,扎心了