OC学习01:事件传递链和响应链

张建 lol

前言

iOS 中只有继承 UIResponder对象 才能够 接收并处理事件UIResponder 是所有响应对象的 基类。继承关系如下:

  • UIApplication -> UIResponder -> NSObject
  • UIViewController -> UIResponder -> NSObject
  • UIView -> UIResponder -> NSObject
  • UIWindow -> UIViww -> UIResponder -> NSObject
  • UIButton -> UIControl -> UIView -> UIResponder -> NSObject

事件链

  • 传递链:由系统向离用户最近的view传递。
    顺序:Appdelegate -> UIApplication -> UIWindow -> UIViewController -> subViews

  • 响应链:由离用户最近的view向系统传递。
    顺序: suberViews –> UIViewController –> UIWindow –> UIApplication –> AppDelegate

传递链

  • 事件传递的两个核心方法
1
2
3
4
// 返回哪个视图进行事件响应
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// 判断某一个点击的位置是否在视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
  • 其中 UIView 不接受事件处理的情况有

    • hidden = YES 视图被隐藏
    • userInteractionEnabled = NO 不接受响应事件
    • alpha <= 0.01,透明视图不接收响应事件
    • 子视图超出父视图范围
    • 需响应视图被其他视图盖住
    • 是否重写了其父视图以及自身的hitTest方法
    • 是否重写了其父视图以及自身的pointInside方法
  • 流程描述

    • 当iOS程序发生触摸事件后,系统会利用 Runloop 将事件加入到 UIApplication 的任务队列中
    • UIApplication 分发触摸事件到 UIWindow
    • 然后 UIWindow 依次向下分发给 UIView
    • UIView 调用 hitTest:withEvent: 方法返回一个最终响应的视图
    • hitTest:withEvent: 方法中就会去调用 pointInside: withEvent: 去判断当前点击的 point 是否在 UIView 范围内,如果是的话,就会去 逆序遍历 它的子视图来查找最终响应的 子视图
    • 遍历的方式是使用 倒序 的方式来遍历子视图,也就是说最后添加的子视图会最先遍历,在每一个视图中都会去调用它的 hitTest:withEvent: 方法,可以理解为是一个 递归调用
    • 最终会返回一个响应视图,如果返回视图有值,那么这个视图就作为最终响应视图,结束整个事件传递;如果没有值,那么就会将UIView作为响应者

响应链

响应者链的事件传递过程:

  • 如果 view 的控制器存在,就传递给控制器处理;如果控制器不存在,则传递给它的 父视图
  • 在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给 UIWindow 对象进行处理
  • 如果 UIWindow 对象也不处理,则将事件传递给 UIApplication 对象
  • 如果 UIApplication 也不能处理该事件,则将该事件丢弃

面试题

  • 实现一个按钮的点击范围扩大效果

思路:自定义一个按钮继承自UIButton,重写 poinstInSide 方法,增大内边距,返回一个新的bounds

1
2
3
4
5
6
7
8
9
10
11
#import "ZJBtn.h"
@implementation ZJBtn
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
// 当前btn大小
CGRect btnBounds = self.bounds;
// 扩大按钮的点击范围,增大内边距
btnBounds = CGRectInset(btnBounds, -50, -50);
// 若点击的点在新的bounds里,返回YES
return CGRectContainsPoint(btnBounds, point);
}
@end
  • 子视图超过父视图部分仍然能响应

思路:正常情况下子视图超出部分是不能响应事件的,需重写 hitTest:withEvent 方法,指定 子视图 可点击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#import "VA.h"

@interface VA ()
@property (nonatomic,strong)UIButton * btn;
@end
@implementation VA
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
[self addSubview:self.btn];
}
return self;
}

- (void)clickBtn{
NSLog(@"%s",__func__);
}

- (UIButton *)btn{
if (!_btn) {
_btn = [[UIButton alloc] initWithFrame:CGRectMake(-50, 20, self.frame.size.width + 100, 100)];
_btn.backgroundColor = [UIColor blueColor];
[_btn addTarget:self action:@selector(clickBtn) forControlEvents:UIControlEventTouchUpInside];
}
return _btn;
}

/*
子视图超过父视图部分,需要点击超出范围的部分也有相应
*/
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 判断btn能否接收事件
if (self.btn.userInteractionEnabled == NO || self.btn.hidden == YES || self.btn.alpha <= 0.01) {
return nil;
}
// 把当前点转换成btn坐标系上的点
CGPoint btnP = [self convertPoint:point toView:self.btn];
// 当触摸点在btn上时,才让按钮相应事件
if ([self.btn pointInside:btnP withEvent:event]) {
NSLog(@"%@",NSStringFromCGPoint(btnP));
return self.btn;
}
NSLog(@"父视图相应");
return [super hitTest:point withEvent:event];
}

@end
  • Post title:OC学习01:事件传递链和响应链
  • Post author:张建
  • Create time:2023-03-01 17:30:34
  • Post link:https://redefine.ohevan.com/2023/03/01/OC/OC学习01:事件传递链和响应链/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
On this page
OC学习01:事件传递链和响应链