概述
在iOS中,视图的层级一般都是父视图->添加各种视图。这个时候某个视图(子视图)上有个按钮,需要我们交互。但是有时候我们会发现无论如何都没有反应。这时候可能就是我们对iOS的事件传递响应还有些迷茫。
1.事件的传递:简单的来说就是事件的传递顺序。他是系统向可响应的离用户最近的视图传递。大致流程就是UIKit -> ...-> root view -> ...->intial view。(方式是从上到下传递)。
2.事件的响应:在我们的视图中一般都是树状结构,有层级关系。那么这时候用户点击了某个控件,所触发的是子视图还是父视图,这种有一个先后的关系,就构成了一个链条,我们就叫做"响应者链条"。响应的大致顺序就是,首先查看initial view是否能够处理这个事件,如果不能事件上传给其父视图,如果上级视图仍然不能够处理则会继续上传,一直传递到视图的控制器,那么首先判断该控制器的根视图view是否能够处理此事件,如果不能那么继续上传(对于目前本身的视图控制器本身还在另一个视图控制器中,则继续交由给其副控制器的跟视图继续处理,如果不能那么就要交给副控制器的控制器来处理)一直到window,如果还是不能处理,那么就要交给application处理,还是不能那么就被丢弃(传递方式是从下到上传递)。
iOS中的事件
1.触摸事件
响应者对象:在iOS中,只要是继承UIResponder的对象都可以接收并处理事件。在iOS中提供了一些方法来处理触摸事件。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; // 开始触摸View时会调用一次
- (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管理的事件队列中(先进先出)-> UIApplication从事件队列中拿出最前的事件将之分发出去,通常是首先发送事件给应用程序的主窗口->主窗口会找到一个最合适的视图来处理触摸事件->找到合适的视图控件后,就会调用控件的上述方法中的一个或者多个来处理具体的事件处理。
事件的传递:主窗口先判断能不能接受这个触摸事件,如果不能,就直接return
主窗口可以接受,传递给子视图,继续判断,继续传递,循环直到没有能够符合响应的子控件,那么这时候的就会认为由自己来处理这个事件最合适。
也有不能响应的情况
1.不允许交互
2.控件隐藏
3.透明度过低(<0.01)
注意:如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件。
如何寻找最适合的控件来处理事件
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
当只要有事件传递给这个控件,这个控件就会调用
hitTest: withEvent:
其作用是寻找并返回最合适的view,不管这个控件能不能处理事件,也不管触摸点是不是在这个空间上,都会先接收事件,然后调用方法。
所以这里我们就有了可操作空间,因为不管点击事件发生在哪里,最终能够处理事件的view都是这个方法返回的view。通过重写这个方法我们可以拦截整个事件的传递过程,同时可以指定处理事件的view,(如果这个方法返回的是nil,那么调用该方法的控件本身以其子控件均不能处理事件,只能由其父视图来处理事件)
所以事件的传递顺序:产生触摸事件 - UIApplication事件队列 - [UIWindow hitTest:withEvent:] -> 返回更合适的View -> [子控件 hitTest:withEvent:] -> 返回最合适的View ...、
所以这里我们可以得到的结论就是 不管子控件是不是最合适的view 都会调用hitTest方法,如果不是最合适的view,会返回nil,同时认定其父视图是最合适的view。
小技巧:在父控件中返回最合适的子控件,因为如果在自己返回自己,有可能两个视图b,c同时加载a上,当设置b为最合适的view,这时候如果我们在b中返回自己,可能我们点击到c这时候b还没来及返回系统就已经定位到了c。
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判断下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断下点在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历子控件数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 获取子控件
UIView *childView = self.subviews[i];
// 坐标系的转换,把窗口上的点转换为子控件上的点
// 把自己控件上的点转换成子控件上的点
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
// 如果能找到最合适的view
return fitView;
}
}
// 4.没有找到更合适的view,也就是没有比自己更合适的view
return self;
}
通过重写view的hitTest方法,即可找到最合适的view
另一个比较重要的方法
pointInside: withEvent:
方法是用来判断我们触摸事件的点位置是否在当前view上,如果返回NO说明是不在当前view坐标系上,同时自然是不能够处理事件的。
事件的响应