iOS的工作分为3大类型

  • Touch Events(接触工作)
  • Motion Events(运动工作,比方重力感应和摇一摇等)
  • Remote Events(长途工作,比方用耳机上得按键来操控手机)

在开发中,最常用到的便是Touch Events(接触工作) ,基本贯穿于每个App中,也是本文的主角~ 因而文中所说工作均特指接触工作。

接下来,记载、触及的问题大致包含:

  • 工作是怎样寻觅工作的最佳呼应者
  • 工作的呼应及在呼应链中的传递

寻觅工作的最佳呼应者(Hit-Testing)

当咱们接触屏幕的某个可呼应的功用点后,终究都会由UIView或者承继UIView的控件来呼应

那咱们先来看下UIView的两个办法:


 // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//回来寻觅到的终究呼应这个工作的视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  
// default returns YES if point is in bounds
//判别某一个点击的方位是否在视图规模内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   

每个UIView目标都有一个hitTest: withEvent:办法,这个办法是Hit-Testing进程中最核心的存在,其作用是问询工作在当时视图中的呼应者,一起又是作为工作传递的桥梁。

看看它是什么时候被调用的

iOS事件的传递机制|【寻找事件最佳响应者、事件在响应者链上的传递】(转)

  • 当手指接触屏幕,UIApplication接收到手指的接触工作之后,就会去调用UIWindowhitTest: withEvent:办法
  • hitTest: withEvent:办法中会调用pointInside: withEvent:去判别当时点击的point是否归于UIWindow规模内,假如是,就会以倒序的办法遍历它的子视图,即越后增加的视图,越先遍历
  • 子视图也调用自身的hitTest: withEvent:办法,来查找终究呼应的视图

再来看个示例:

iOS事件的传递机制|【寻找事件最佳响应者、事件在响应者链上的传递】(转)

视图层级如下(同一层级的视图越在下面,表明越后增加):


A
├── B
│   └── D
└── C
    ├── E
    └── F

现在假定在E视图所在的屏幕方位触发一个接触,App接收到这个接触工作工作后,先将工作传递给UIWindow,然后自下而上开始在子视图中寻觅最佳呼应者。工作传递的次序如下所示:

iOS事件的传递机制|【寻找事件最佳响应者、事件在响应者链上的传递】(转)

  • UIWindow将工作传递给其子视图A
  • A判别自身能呼应该工作,持续将工作传递给C(由于视图C比视图B后增加,因而优先传给C)。
  • C判别自身能呼应工作,持续将工作传递给F(同理F比E后增加)。
  • F判别自身不能呼应工作,C又将工作传递给E。
  • E判别自身能呼应工作,一起E现已没有子视图,因而终究E便是最佳呼应者。

以上,便是寻觅最佳呼应者的整个进程。

接下来,来看下hitTest: withEvent:办法里,都做些了什么?

咱们现已知道工作在呼应者之间的传递,是视图经过判别自身能否呼应工作来决议是否持续向子视图传递,那么判别呼应的条件是什么呢?

视图呼应工作的条件:

  • 允许交互userInteractionEnabled = YES
  • 制止躲藏:hidden = NO
  • 透明度:alpha > 0.01
  • 接触点的方位:经过pointInside: withEvent:办法判别接触点是否在视图的坐标规模内

代码的体现大约如下:


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    //3种状况无法呼应工作
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
     //接触点若不在当时视图上则无法呼应工作
    if ([self pointInside:point withEvent:event]) {
         //从后往前遍历子视图数组
        for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
            // 坐标系的转化,把接触点在当时视图上坐标转化为在子视图上的坐标
            CGPoint convertedPoint = [subView convertPoint:point fromView:self];
             //问询子视图层级中的最佳呼应视图
            UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                 //假如子视图中有更适宜的就回来
                return hitTestView;
            }
        }
         //没有在子视图中找到更适宜的呼应视图,那么自身便是最适宜的
        return self;
    }
    return nil;
}

说了这么多,那咱们能够运用hitTest: withEvent:来搞些什么工作呢

使超出父视图坐标规模的子视图也能呼应工作

iOS事件的传递机制|【寻找事件最佳响应者、事件在响应者链上的传递】(转)

视图层级如下:

css
A
├── B

如上图所示,视图B有一部分是不在父视图A的坐标规模内的,当咱们接触视图B的上半部分,是不会呼应工作的。当然,咱们能够经过重写视图AhitTest: withEvent:办法来解决这个需求。


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    //假如找不到适宜的呼应者
    if (view == nil) {
        //视图B坐标系的转化
        CGPoint newPoint = [self.deleteButton convertPoint:point fromView:self];
        if (CGRectContainsPoint(self.deleteButton.bounds, newPoint)) {
            // 满意条件,回来视图B
            view = self.deleteButton;
        }
    }
    return view;
}

视图AhitTest: withEvent:办法中判别接触点,是否位于视图B的视图规模内,假如归于,则回来视图B。这样一来,当咱们点击视图B的任何方位都能够呼应工作了。

工作的呼应及在呼应链中的传递

经历Hit-Testing后,UIApplication现已知道工作的最佳呼应者是谁了,接下来要做的工作便是:

  • 将工作传递给最佳呼应者呼应
  • 工作沿着呼应链传递

工作传递给最佳呼应者

最佳呼应者具有最高的工作呼应优先级,因而UIApplication会先将工作传递给它供其呼应。

UIApplication中有个sendEvent:的办法,在UIWindow中相同也能够发现一个相同的办法。UIApplication是经过这个办法把工作发送给UIWindow,然后UIWindow经过相同的接口,把工作发送给最佳呼应者。

寻觅工作的最佳呼应者一节中点击视图E为例,在EViewtouchesBegan:withEvent:上打个断点检查调用栈就能看清这一进程:

iOS事件的传递机制|【寻找事件最佳响应者、事件在响应者链上的传递】(转)

当工作传递给最佳呼应者后,呼应者呼应这个工作,则这个工作到此就完毕了,它会被开释。假定呼应者没有呼应这个工作,那么它将何去何从?工作将会沿着呼应链自上而下传递。

注意: 寻觅最佳呼应者一节中也提到了工作的传递,与此处所说的工作的传递有本质区别。上面所说的工作传递的目的是为了寻觅工作的最佳呼应者,是自下而上(父视图到子视图)的传递;而这儿的工作传递目的是呼应者做出对工作的呼应,这个进程是自上而下(子视图到父视图)的。前者为“寻觅”,后者为“呼应”。

工作沿着呼应链传递

在UIKit中有一个类:UIResponder,它是一切能够呼应工作的类的基类。来看下它的头文件的几个特点和办法


NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif
--------------省掉部分代码------------
  // 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;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

UIApplication,UIViewController和UIView都是承继自它,都有一个nextResponder办法,用于获取呼应链中当时目标的下一个呼应者,也经过nextResponder来串成呼应链

在App中,一切的视图都是根据树状层次结构组织起来的,因而,每个View都有自己的SuperView。当一个ViewaddSuperView上的时候,它的nextResponder特点就会被指向它的SuperView,各个不同呼应者的指向如下:

  • UIView 若视图是操控器的根视图,则其nextResponder为操控器目标;不然,其nextResponder为父视图。
  • UIViewController 若操控器的视图是window的根视图,则其nextResponder为窗口目标;若操控器是从别的操控器present出来的,则其nextResponder为presenting view controller。
  • UIWindownextResponder为UIApplication目标。
  • UIApplication 若当时使用的app delegate是一个UIResponder目标,且不是UIView、UIViewController或app自身,则UIApplication的nextResponder为app delegate。

这样,整个App就经过nextResponder串成了一条链,也便是咱们所说的呼应链,子视图指向父视图构成的呼应链。

看一下官网关于呼应链的示例展现

iOS事件的传递机制|【寻找事件最佳响应者、事件在响应者链上的传递】(转)

若接触发生在UITextField上,则工作的传递次序是:

  • UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegte

图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponderUIViewController目标;若是直接addUIWindow上的,则其nextResponderUIWindow目标。

呼应者关于工作的阻拦以及传递都是经过touchesBegan:withEvent:办法操控的,该办法的默认实现是将工作沿着默认的呼应链往下传递。

呼应者关于接收到的工作有3种操作:

  • 不阻拦,默认操作 工作会自动沿着默认的呼应链往下传递
  • 阻拦,不再往下分发工作 重写touchesBegan:withEvent:进行工作处理,不调用父类的touchesBegan:withEvent:
  • 阻拦,持续往下分发工作 重写touchesBegan:withEvent:进行工作处理,一起调用父类的touchesBegan:withEvent:将工作往下传递

因而,你也能够经过touchesBegan:withEvent:办法搞点工作~

总结

接触工作先经过自下而上(父视图–>子视图)的传递办法寻觅最佳呼应者,

然后以自上而下(子视图–>父视图)的办法在呼应链中传递。