iOS事件传递和响应机制

开场白

iOS开发这么多年,其实从来就没关心过时间传递和响应机制这么个事。当我看到这篇文章史上最详细的iOS之事件的传递和响应机制-原理篇后,发现其中有很多东西可以细细品味一下的。

1.简述事件流程

整个事件传递和处理流程,简单概括为:

事件-事件传递到指定界面-找到可响应的界面-响应

我开始的理解误区就是‘传递到指定界面’和‘可响应界面’理解成同一个界面了,造成我在看上面的文章的时候,有些混乱。其实这两个可以是两个界面。

例如:我在touchBegin一个view的时候,需求是view不响应,而superview响应。而事件传递是传递到view中。这种情况两个view就是不相同的界面。

2.事件传递

  1. 当有用户触摸屏幕的时候产生事件,系统硬件进程获取到这个事件,并处理封装保存在系统中,由于系统硬件进程和app进程是两个不同的进程,所以使用进程间的端口通信。
  2. 系统会将这个事件加入到UIApplication的事件管理队列中,事件从队列中出队后通常会发送给app的keywindow处理。
  3. keywindow会找到一个最适合的视图去处理事件。也就是从super控件到子控件中。
  4. 简单总结:UIApplication->window->寻找处理事件最合适的view

2.1 找到适合视图的过程

  1. 首先keywindow是可以接受事件的
  2. 判断是否事件发生在自己的可视范围内,例如:触摸点击在自己的bound中。
  3. 子控件数组按照从后往前的顺序查找适合的子控件,重复步骤1和步骤2。(从后往前的意思就是subviews中从最后一个元素开始向前找,这种方式可以减少遍历次数,提高效率)
  4. 找到子控件后再继续找它的子控件。
  5. 如果没有找到合适的子控件,那么当前的控件就是最适合的。

2.2 UIView不能接收触摸事件的三种情况

  • 不允许交互:userInteractionEnabled = NO,例如UIImageView中addSubview一个button,button的点击是没有反应的。
  • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
  • 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。

如果不想让view处理事件,而是想让superview处理,就可以吧view的userInteractionEnabled设置为no。

2.3 最适合的子控件

系统api中提供了两个方法,

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

为了方便:hitTest:withEvent:方法在文章后续用hitTest代替,pointInside:withEvent:用pointInside代替

通过注释了解到hitTest方法是递归的调用pointInside方法。point是在接受控件坐标系内的。

底层的事件传递实现就是:
产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view->...->返回最合适的view

2.4 拦截事件传递

我们可以重写hitTest方法,来拦截系统的事件传递,让指定的view处理事件。例如自定义view中,想让view中的一个subview处理事件,就可以在自定义view中重写该方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if ([self pointInside:point withEvent:event]) {
        return self;
    }
    
    return nil;
}

示例代码我返回的是self,这里可以改成指定的subview,或者遍历subview中的一个。

3. 响应链

在很多文章中都看到了这张图,不清楚是不是官方,但图片中的逻辑是没有问题的,ios控件间的摆放都是有层级关系的,这张图表示的很清晰。响应者对象就是继承与UIResponder的子类们。

3.1 UIResponder的子类

UIResponder的子类有一下几个:

  • AppDelegate
  • UIApplication
  • UIViewController
  • UIView

p.s. UIWindow的父类是UIView

3.2 nextResponder

UIResponder的子类是通过nextResponder进行连接的。

响应链创建方式,本人个人理解,应该是链表的头插法形式:

  1. AppDelegate作为整个链的根基,是第一个被创建出来的,在main函数中被调用。它的nextResponder为nil。当前链表的状态:AppDelegate->nil
  2. 系统提供给我们的UIApplication单例,响应链变为:UIApplication->AppDelegate->nil
  3. UIApplication会创建keyWindow,是UIWindow类型,父类是UIView,也是UIResponder的子类,所以响应链变为:keyWindow->UIApplication->AppDelegate->nil
  4. keyWindow中会设置一个rootViewController,是UIViewController类型,是UIResponder子类,rootViewController->keyWindow->UIApplication->AppDelegate->nil
  5. rootViewController中有view,我们在开发中把自定义的view加载vc的view中,最终响应链为:自定义view->superview->rootViewController->keyWindow->UIApplication->AppDelegate->nil

这里只是简单举个例子,其实项目中会有更复杂的层级关系。

3.3 官方文档可以证明

很多人会问如何证明呢,我们来看看官方文档中的解释:


Summary

Returns the next responder in the responder chain, or nil if there is no next responder.

返回响应者链中的下一个响应者,如没有下一个响应者返回nil。

Disussion

The UIResponder class does not store or set the next responder automatically, so this method returns nil by default. Subclasses must override this method and return an appropriate next responder. For example, UIView implements this method and returns the UIViewController object that manages it (if it has one) or its superview (if it doesn’t). UIViewController similarly implements the method and returns its view’s superview. UIWindow returns the application object. The shared UIApplication object normally returns nil, but it returns its app delegate if that object is a subclass of UIResponder and has not already been called to handle the event.

UIResponder类不会自动存储和设置下一个响应者(next responder),这个方法默认返回nil。子类必须复写这个方法并且返回一个合适的下一个响应者。例如,UIView实现这个方法,如果是被UIViewController对象管理的下一个响应者就是UIViewController;如不哦不是被UIViewController对象管理的,下一个响应者就是superview。UIViewController同样实现这个方法,并且返回它自己view的superview。UIWindow返回application对象。shared UIApplication对象通常返回nil,但是如果该对象是一个UIRespnder的子类并且还没有被调用去处理事件,它返回的是app的delegate。

3.4 事件响应链中的传递

通过上面例子中的响应链自定义view->superview->rootViewController->keyWindow->UIApplication->AppDelegate->nil的顺序,逐层向后查找可做响应的响应者(UIResponder子类)。

如果多层有实现了UIResponder的相关方法,例如touchesBegan,这多层都可以响应。

举个例子:
vc中init一个自定义的TestView,并且在vc和TestView中都实现了touchesBegan方法

vc部分代码:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.view.backgroundColor = UIColor.lightGrayColor;
    
    TestView *view1 = [[TestView alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
    view1.tag = 1;
    view1.backgroundColor = [UIColor redColor];
    [self.view addSubview:view1];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
    [super touchesBegan:touches withEvent:event];
}

TestView部分代码:

@implementation TestView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
    [super touchesBegan:touches withEvent:event];
}

运行后的效果:


点击红色区域后查看控制台:



TestView和VC的touchesBegan方法都调用了。

注意:TestView中的touchesBegan要调用super touchesBegan,如果不调用,vc中无法打印。因为不调用就不会继续查找响应链中后续的响应者了。vc中touchesBegan中调用了super也是同理目的。

4. 简单总结

事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)。

5. 应用场景

参考这篇文章:iOS事件响应链中hitTest的应用示例

其中包括:

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