iOS -响应者链

引用:

https://blog.csdn.net/weixin_72437555/article/details/138610888
https://zhuanlan.zhihu.com/p/476477484

截屏2024-09-16 16.37.55.png
截屏2024-09-16 16.39.18.png
截屏2024-09-16 16.39.33.png
截屏2024-09-16 16.42.49.png
image.png
截屏2024-09-16 16.56.48.png
截屏2024-09-16 16.56.10.png
截屏2024-09-16 16.56.17.png

总结

. 当触摸事件发生后,系统会自动生成一个UIEvent对象,记录事件产生的时间和类型
. 然后系统会将UIEvent事件加入到一个由UIApplication管理的事件队列中
. 然后UIApplication将事件分发给UIWindow,主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件
. 不断递归调用hitTest方法来找到第一响应者
. 如果第一响应者无法响应事件,那么按照响应者链往上传递,也就是传递给自己的父视图
. 一直传递直到UIApplication,如果都无法响应则事件被丢弃

触摸事件由触屏生成后如何传递到当前应用?

系统响应阶段

1.指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。

2.IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。

mach port 进程端口,各进程之间通过它进行通信。
SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。
3.SpringBoard进程因接收到触摸事件,将触摸事件交给前台app进程来处理。

APP响应阶段

1.APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调。

2.source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。

3.source0回调内部将触摸事件添加到UIApplication对象的事件队列中。事件出队后,UIApplication开始一个寻找最佳响应者的过程,这个过程又称hit-testing,另外,此处开始便是与我们平时开发相关的工作了。

4.寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了。

5.触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。

至此,这个触摸事件的使命就算终结了。runloop若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。

参考博客事件传递与响应 详解(精通iOS系列)
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/weixin_72437555/article/details/138610888

以下内容重复:

所以事件的传递顺序是这样的:
  产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。

当UIApplication发送事件到keyWindow时,keyWindow会调用-hitTest:withEvent:方法来寻找最适合处理事件的视图。假设事件已经传递到某视图view,选择出能响应视图的逻辑如下:

1、首先会判断该视图自身能否处理该触摸事件,如果不能响应,则不通过pointInside方法,则hitTest方法直接返回nil;
2、如果该View可以响应,则调用-pointInside:withEvent:判断是否在显示区域上,如果不在其区域中,则返回NO,同时-hitTest:withEvent:也返回nil;
3、如果步骤2中返回YES,表示在当前View的范围中,接着先倒序遍历该视图的子视图;
4、如果步骤3中没有子视图,或者没有任何一个子视图能够响应该触摸事件,则返回该视图自身,表示只有自身可以处理该事件。

作者:夜凉听风雨
链接:https://www.jianshu.com/p/777487a6d87c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
** 技巧:**

想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
原因在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题。因为会存在这么一种情况:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是触摸点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!

1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…
3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

事件的链有两条:事件的响应链;Hit-Testing 时事件的传递链。

响应链:由离 户最近的view向系统传递。 initial view –> super view –> .....–> view controller –> window –> Application –> AppDelegate

Hit-Testing 链:由系统向离 户最近的view传递。 UIKit –> active app's event queue –> window –> root view –>......–>lowest view

事件的传递与响应:

1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。

2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法

作者:VV木公子
链接:https://www.jianshu.com/p/2e074db792ba
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


iOS中的事件

触摸事件,加速事件(摇一摇),远程控制事件(耳机线控,窗口播放)

以最常见的触摸事件为例,当触摸手机屏幕时操作系统会将这个事件添加到由UIApplication管理的事件队列中(FIFO)UIApplication发送事件到应用程序的主窗口(Window)Window会在图层结构中找到最合适的图层来处理事件。

UIResponder

UIResponder类是专门用来响应用户的操作处理各种事件的,iOS中大部分控件都继承自UIResponder,默认响应事件的方法如下(触摸事件)

- (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;//中断,被手势或者系统中断
事件传递链

UIApplication传递事件到当前Window是明确的,接下来就是从Window开始找最佳响应视图,此过程有两个重要的方法:

hitTest方法继承自UIView(UIWindow是继承自UIView的)。从UIApplication开始调用Window的hitTest方法,默认是递归调用的。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    return [super pointInside:point withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    return [super hitTest:point withEvent:event];
}

传递过程如下:

1.系统从UIApplication开始,当前window调用hitTest,hitTest内部会通过以下条件判断window能否能响应事件

不允许交互:userInteractionEnabled=NO
隐藏:hidden = YES
透明度:alpha < 0.01,alpha小于0.01为全透明
2.如果能响应,该函数内部会调用pointInside判断当前触摸点是不是在视图范围内

3.如果在window范围内,开始反向遍历window的子视图列表subviews,遍历的同时会调用subviews中每个子视图的hitTest,判断逻辑和上面的一样,如果找到循环就会停止。

4.此过程会递归,直到找到最外层合适的view,最后返回的view就是最佳响应视图。

一种hitTest可能的实现方式如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    if (!self.userInteractionEnabled|| self.hidden || self.alpha == 0.0){
        return nil;
    }
    if (![self pointInside:point withEvent:event]){
        return nil;
    }
    // 后加入的视图在图层上方,所以反向遍历是合理的
    NSInteger count = self.subviews.count;
    for (NSInteger i = count - 1; i >= 0; i--)
    {
        UIView *view = self.subviews[i];
        // 坐标的转换
        CGPoint subPoint = [self convertPoint:point toView:view];
        // 继续递归
        UIView *lastView = [view hitTest:subPoint withEvent:event];
        if (lastView)
        {
            return lastView;
        }
    }
    return self;
}

以上,这就是事件传递过程,由内往外的传递过程(从window开始到最外层视图 )

此过程查找结束返回最终的view,UIApplication会调用UIWindow的sendEvent,从而触发对应的响应方法:

PS:这里通过在UIWIndow中重写sendEvent而不调用super的实现,你会发现所有的点击事件都不会触发

  • (void)sendEvent:(UIEvent *)event;
    以下是需要注意的点:

实际调用hitTest过程,系统为了找到精准的触摸点会多次调用

如果重写hitTest返回self,传递过程就会终止,当前view就是最合适的view;返回nil,传递也会终止,父视图superView就是最合适的view

如果遍历subviews的过程都没找到合适的view,那么subviews中的子view的hitTest会都会被被调用一次

hitTest会调用pointInside判断当前视图是否在点击区域,所以超出父视图边界的控件无法响应事件

同一个view上的两个子视图有重叠部分,后加入的视图会被加入到事件传递链

事件响应链

首先,响应者链中的各个响应者都继承自UIResponder,常见的UIView,viewController,UIWindow以及AppDelegate都继承自UIResponder。响应者链上的响应者在hitTest过程中就已经确定,可以通过迭代nextResponder查看所有的响应者。

事件响应链如下:

通过hitTest返回的view为当前事件的第一响应者,nextResponder为上一个响应者

如果当前view默认不去重写,或者重写调用了父类的实现,响应就会就会沿着响应者链向上传递(上一个响应者一般是superView,可以通过nextResponder属性获取上一个响应者)

如果上一个响应者是viewController,由viewController的view处理,view本身没处理,则传递给viewController本身

重复上述过程,直到传递到window,window如果也不能处理,传递到UIApplication,如果UIApplication的delegate继承自UIResponder,则交给delegate处理,delegate也不处理最后丢弃

以上就是响应者链,事件响应过程是从外向内传递,和事件传递的过程正好相反

通过遍历查找所有响应者:

UIResponder *respon = self;
while (respon) {
    NSLog(@"%@",respon);
    respon = respon.nextResponder;
}

作者:你duck不必呀
链接:https://www.jianshu.com/p/70e8bc099966
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 一篇搞定事件传递、响应者链条、hitTest和pointInside的使用发生触摸事件后,系统会将该事件加入到一个...
    克鲁德李阅读 4,801评论 0 1
  • 一、响应者链(Responder Chain) 先来说说响应者对象(Responder Object),顾名思义,...
    像小强一样活着阅读 11,769评论 8 76
  • 先来明确几个概念 响应者对象可以进行事件处理的对象。用户进行了某个操作,系统会将该操作包装成一个Event事件对象...
    yaqiong阅读 1,358评论 0 0
  • 一、概述 iOS 响应者链(Responder Chain)是支撑 App 界面交互的重要基础,点击、滑动、旋转、...
    小道萧兮阅读 8,186评论 2 12
  • iOS中的响应者链是指UIKit 生成的UIResponder对象组成的链表,它是iOS里一切事件相关(触摸事件、...
    左左4143阅读 3,465评论 0 0