IOS触摸事件的分发和响应链(二)

事件的响应链(The Responder Chain)

响应者对象(The Responder Object)

响应者链由响应者对象组成,响应者对象都是UIResponder的子类。

许多类型的事件依赖于用于事件传递的响应者链。响应者链是一系列链接的响应者对象,它从第一个响应者(responder)开始,并以应用程序对象(application object)结束。如果第一响应者不能处理事件,它将事件转发到响应者链中的下一个响应者。

响应者对象是能够响应并处理事件的对象。 UIResponder类是所有响应者对象的基类,它定义了编程接口,不仅用于事件处理而且用于公共响应者行为(common responder behavior)。 UIApplication,UIViewController和UIView类的实例是响应者,这意味着所有视图和大多数键控制器对象都是响应者

>注意核心动画层不是响应者。

第一响应者被指定为第一个接收事件的对象。通常,第一响应者是视图对象。一个对象通过做两件事情成为第一个响应者:

  • 重写canBecomeFirstResponder方发并且返回YES。
  • 接收becomeFirstResponder消息。如果需要,响应者对象可以向自身发送此消息。

响应者链的路径(The Responder Chain's Delivery Path)

响应者链遵循着明确的(事件响应的)传递路径(The Responder Chain Follows a Specific Delivery Path)

如果初始对象(hit-test view或第一个响应者)不处理事件,则UIKit会将事件传递给响应链中的下一个响应者。每个响应者可以决定是自己处理该事件还是将通过调用nextResponder方法将其传递给自己的下一个响应者。这个过程将会一直进行,直到有响应者对象处理了事件或事件传递到响应者连的最顶端并且没有更多的响应者了才会停止。

5. The responder chain on iOS

响应者链序列在iOS检测到事件并将其传递到初始对象(通常是视图)时开始。 初始视图有第一个处理事件的机会。 上图显示了两种应用配置的两种在不同app设置下的不同的事件传递路径。 应用程序的事件传递路径取决于其特定结构,但所有事件传递路径都遵循相同的(事件传递)的探索法(heuristics)。

对于上图5左侧的应用程序,事件响应遵循以下路径:

  1. 初始view尝试处理事件或消息。如果它不能处理事件,它将事件传递到其父视图superView;
  2. superview尝试处理事件。如果superview不能处理事件,它将事件传递到其上层试图(superView);
  3. 视图控制器视图层级中的最顶层视图尝试处理事件。如果最顶层视图不能处理事件,它将事件传递给它的视图控制器(controller);
  4. controller处理事件,如果不能,则将事件传递到window;
  5. 如果window不能处理事件,它将事件传递给singleton app object;
  6. 如果UIApplication的单例实例对象依然无法处理事件,则该事件会被丢弃。

对于上图5右侧的应用程序遵循的路径

相比于左侧app所遵循的事件响应的路径,右侧稍微有些不同,但所有事件传递路径遵循以下启发式:

  1. view将事件向上传递到其controller的视图层次结构,直到它到达最顶层的视图;
  2. 最顶层view将事件传递到其controller;
  3. controller将事件传递到其最顶层视图的superView;
  4. 重复步骤1-3,直到事件到达root controller;
  5. root controller将事件传递到window;
  6. window将事件传递给singleton app object。

响应触摸事件

UIResponder提供给了子类下面处理触摸事件的接口

// 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);

由于UIGestureRecognizer,所以在这里就没有总结关于在touches...系列方法中实现拖动,捏合等效果。

将事件传递给'controller'

// GreenView.m 中重写下面两个方法(不需要做其他处理的话也可以不重写)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
}

然后在controller中添加

greenView = [[GreenView alloc] initWithFrame:CGRectMake(150, 200, 75, 75)];  
[self.view addSubview:greenView];

然后重写- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event方法

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    if (CGRectContainsPoint(greenView.frame, [touch locationInView:self.view])) {
        NSLog(@"点了greenView");
    } else {
        NSLog(@"点了白色背景view");
    }
    [super touchesEnded:touches withEvent:event];
}

在view中寻找controller

在自定义RedView.m中:

-(UIViewController*)parentController{
    UIResponder *responder = [self nextResponder];
    while (responder) {
        if ([responder isKindOfClass:[UIViewController class]]) {
            return (UIViewController*)responder;
        }
        responder = [responder nextResponder];
    }
    return nil;
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if ([self parentController]) {
        NSLog(@"%@",NSStringFromClass([[self parentController] class]));
        if ([self parentController].navigationController) {
            TextViewController *tc = [[TextViewController alloc] init];
            [[self parentController].navigationController pushViewController:tc animated:YES];
        }
    }
    [super touchesEnded:touches withEvent:event];
}

有几点说明:

  • [super touchesEnded:touches withEvent:event]; 和 [self.nextResponder touchesEnded:touches withEvent:event]; 在大部分情况下前者更好一点,当有其他需要特殊对待的问题,后者估计干脆一些
  • [super touchesEnded:touches withEvent:event];在重写方法中的调用,尽量都写上比较好
  • 在view中直接通过nextResponder对象来获取controller,这种做法感觉不怎么合适,觉得view做了超出本身职责范围的事。
  • 以上三点属于个人揣测,还有待验证。

其实上面这两个小栗子很浅显,但是由此点出发可以找到更多解决问题的思路。

总结

这两篇学习记录分开写觉着有点没必要,因为本身在官方文档直接说的是Event Delivery: The Responder Chain。从找到hit-test view到响应并处理事件是一个整体。:

传递:由系统把事件传向离用户最近的view。UIKit –> active app’s event queue –> window –> root view –>……–>lowest view–>hit-test view

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

概述图

最后,在事件的响应链里面可以以简单的方式实现许多比较复杂的逻辑。希望以后可以在深入学习并且灵活应用。

参考链接

作者介绍

  • 张亚斌 iOS 高级开发工程师

微鲤技术团队

微鲤技术团队承担了中华万年历、Maybe、蘑菇语音、微鲤游戏高达3亿用户的产品研发工作,并构建了完备的大数据平台、基础研发框架、基础运维设施。践行数据驱动理念,相信技术改变世界。