事件响应链

通过事件传递找到最合适的view为第一响应者,其父view或viewcontroller则为第二响应者,以此递推,形成响应链。即pointInside返回true的view加上它们的控制器、UIWindow和UIApplication共同构成响应链。响应链的作用是找到一个和多个合适的view顺序的响应事件。响应链有如下规则:

  1. 超出父view范围的子view不能hitTest命中
  2. 父view禁止响应,影响到所有子view
  3. hitTest和touchesBegan是相反的顺序,即hitTest  :v1->v2->v3, 若都命中则touchesBegan  :v3->v2->v1

了解以下几个东西,能帮助理解响应链:

UIResponder

UIResponder是响应链的载体, 是UIKit的一个类,负责处理响应事件,是很多上层基本类的基类:

@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, CALayerDelegate>
@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>

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;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
...

hitTest

histTest简单而言,作用就是判断某个view是否被点击,通过下面一段代码,可以做到其判断过程:

- (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])   return nil; 
    // 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件 
    int count = self.subviews.count; 
    for (int i = count - 1; i >= 0; i--)
     {
       UIView *childView = self.subviews[i]; 
       CGPoint childPoint = [self convertPoint:point toView:childView]; 
       UIView *fitView = [childView hitTest:childPoint withEvent:event]; 
       if (fitView) { return fitView; } 
     } 
     // 没有找到比自己更合适的view 
     return self; 
}

histTest是一个递归调用的过程:

  1. 判断自己能否接收触摸事件(是否不接受用户交互、隐藏、透明),否则返回nil
  2. 调用pointInside判断触摸点是否在自己范围,否则返回nil
  3. 广度优先搜索遍历子view重复上述过程
  4. 若子view没有histTest命中,则返回当前view通过重写histTest可以实现一些非常有用的功能,例如扩大按钮点击域、修改事件响应优先级等

手势

手势识别在 iOS 中非常重要,它极大地丰富了用户与iOS程序的交互方式。其使用步骤一般为: 创建手势->实现手势回调->为指定view添加手势,iOS中主要有多达7种手势。需要注意手势冲突等问题

手势与响应者链

  1. 手势与响应者链有一些差别,触摸事件首先会传递到手势上,如果手势识别成功,就会取消事件的继续传递。如果手势识别失败,事件才会被响应链处理
  2. 对于 UIButton,UISwitch,UISegmentedControl,UIStepper、UIPageControl 进行单击操作,如果父视图有轻敲手势需要识别,依然会按照响应链来处理,先响应这些控件的单击事件,这仅适用于与控件的默认操作重叠的手势识别。

UIResponder 和 UIControl

  • UIResponder类:上承NSObject,下接UIView ,UIVIewController ,UIApplacation;响应点,压,滑,主要作用是响应某个动作,执行某个行为。
  • UIControl类:上承UIView,下接UIButton、UISwitch等控件,在继承了UIResponder的属性基础上,还能够相应某个动作,为某个对象,添加动作(比如常见的addTarget函数)

手势冲突

1、如果一个手势A的识别部分是另一个手势B的子部分时,默认情况下A就会先识别,B就无法识别了。我们可以指定某个手势执行的前提是另一个手势失败才会识别执行,这样控制手势识别的响应顺序:

- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;

例子如下:

UITapGestureRecognizer *tapGesture0 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick0:)];
[self.view addGestureRecognizer: tapGesture0];

UIView *testView = [[UIView alloc] initWithFrame:CGRectMake(20, 100, 200, 200)];
testView.backgroundColor = [UIColor redColor];
[self.view addSubview: testView];
UITapGestureRecognizer *tapGesture1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick1:)];
[self.view addGestureRecognizer: tapGesture1];

//设置tapGesture0优先级高于tapGesture1
//[tapGesture1 requireGestureRecognizerToFail: tapGesture0];

-(void)tapClick0:(UITapGestureRecognizer *)tap{
    NSLog(@"点击tap0");
}
-(void)tapClick1:(UITapGestureRecognizer *)tap{
    NSLog(@"点击tap1");
}

可以取消注释//[tapGesture1 requireGestureRecognizerToFail: tapGesture0];查看不同结果

2、如果同一视图需要一次响应多个手势操作,可以实现下面的UIGestureRecognizerDelegate的代理方法,当返回YES的时候,可以同时响应多个手势。

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

响应链另一种用法:ResponderChain

日常开发中,不同 UI 层级之间交互可有多种抉择:

  1. block
  2. 协议代理模式
  3. 通知

但是随着业务复杂提高上面的任何一种方式都会变得非常厚重,例如像直播间这种玩法多样的vc,最终可能会有几十种回调代码,不同回调的层数还各不一致,最终导致代码,此时我们需要考虑一种新的方式来达到我们目的。 假设有个直播控制器:liveVC,他的UI 层级是这样的:

img.png

4 个业务层view分别有各种回调需要listVC 处理,那么除了上述常规方式还有别的简洁的方案吗?答案是肯定的,我们可以基于响应链来达到目的,对 UIResponder 做如下分类:

@implementation UIResponder (Router)
 
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo
{
    [[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}
 
@end

这样在最上层的view 处理交互事件时,直接调用routerEventWithName 函数即可通过 nextResponder 逐级查找到你想要实现交互事件回调的层级了。