美文网首页iOS定制viewiOS
响应链的分析与应用

响应链的分析与应用

作者: 杭研融合通信iOS | 来源:发表于2017-04-03 15:01 被阅读125次

    UI相关的开发和调试中,经常会涉及到点击、触摸、手势等情况的调试,最开始的开发时,也许会用用touchBegan等方法或者网上找找相关解决方案,并没有系统的了解过相关知识。今天主要是讲一下点击、触摸、手势调试的根源——响应链,顺带讲下手势与其关系,深入理解这些,对我们以后调试这些问题都能思路更清晰。

    先从我们日常开发中可能会遇到的问题,我们可以自测下对于响应链的理解是否达标,如果这三个问题都能清晰地知道解决方案,可以忽略下文了:

    • 隐藏键盘可以怎样实现?
    • resignFirstResponder是什么意思?
    • 一个视图A遮挡了视图B,如果让B来响应,而A不响应?

    通过本文,对以上三个问题不仅知道解决方案,而且“知其所以然”。

    触摸事件UIEvent

    首先,我们平常在手机上最简单的一个动作开始:


    我们在手机上的一个触摸或点击,在手机系统中是怎么样一个过程:
    其实首先就是手机屏幕和底层相关软硬件对这个触摸一个解析过程,将这个触摸解析成event,然后iOS系统将这个event事件传递到相应的界面上,由界面来响应我们的操作,给出对应的反馈,这样一个交互过程。这其中传递的过程就是我们今天的要了解的主角。从图上可以看到,从window—view之间,具体是怎么样一个规律呢? 先从这个触摸或点击开始了解一下:

    UITouch与UIEvent

    一次触摸将产生一个UITouch:一个手指离开屏幕前的一系列动作,包含时间戳、所在视图、 力度等信息。
    UIEvent:多个UITouch组成,也就是多个触摸组成。 一个event指的是第一个手指开始触摸到最后一个手指离开屏幕这段时间所有UITouch的总和。
    那么这个UIEvent是如何在系统解析出来后,传递下去呢?哪些对象可以传递UIEvent呢?

    响应者Responder

    响应者就是可以接收到UIEvent的对象,也是可以最终响应用户的操作的对象。iOS开发中主要与四种对象可以作为responder:


    上图的四个类都继承自UIResponder:

    • @interface AppDelegate : UIResponder
    • @interface UIWindow : UIView
    • @interface UIView : UIResponder
    • @interface UIViewController : UIResponder

    UIResponder作为响应者的基类,主要是定义了一些关于响应的属性和方法,用于子类判断或者执行关于响应的操作:

    @interface UIResponder : NSObject <UIResponderStandardEditActions>
    
    #if UIKIT_DEFINE_AS_PROPERTIES
    @property(nonatomic, readonly, nullable) UIResponder *nextResponder;
    #else
    - (nullable UIResponder*)nextResponder;
    #endif
    
    #if UIKIT_DEFINE_AS_PROPERTIES
    @property(nonatomic, readonly) BOOL canBecomeFirstResponder;    // default is NO
    #else
    - (BOOL)canBecomeFirstResponder;    // default is NO
    #endif
    - (BOOL)becomeFirstResponder;
    
    #if UIKIT_DEFINE_AS_PROPERTIES
    @property(nonatomic, readonly) BOOL canResignFirstResponder;    // default is YES
    #else
    - (BOOL)canResignFirstResponder;    // default is YES
    #endif
    - (BOOL)resignFirstResponder;
    
    #if UIKIT_DEFINE_AS_PROPERTIES
    @property(nonatomic, readonly) BOOL isFirstResponder;
    #else
    - (BOOL)isFirstResponder;
    #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);
    
    - (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
    - (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
    - (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
    - (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
    
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
    - (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
    
    - (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
    
    - (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(3_0);
    // Allows an action to be forwarded to another target. By default checks -canPerformAction:withSender: to either return self, or go up the responder chain.
    - (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(7_0);
    
    @property(nullable, nonatomic,readonly) NSUndoManager *undoManager NS_AVAILABLE_IOS(3_0);
    

    这些responder在接受上一个responder传递而来的响应UIEvent,这样依次传递响应的顺序就是一个响应链,如果将响应链顺序反向:就是UIEvent通过hittest遍历的过程链。 责任传递就是处于应该响应顺序上的上一个对象有责任处理这个UIEvent,如果他不处理,就轮到响应链上的下一个来处理。也就是说刚才UIEvent的传递过程和响应链是两个互逆的顺序,但是这个具体哪个能作为响应者,该如何确定呢?

    其实系统是通过hittest方法来决定哪个view作为最终响应用户操作的载体。hittest是UIView的一个扩展方法,与其搭配的就是pointInside方法,二者功能可以确定点击或者触摸的点是否在view的范围内,通过hittest的返回值告诉系统当前view是否可以作为响应者。

    @interface UIView(UIViewGeometry)
    
    // animatable. do not use frame if view is transformed since it will not correctly reflect the actual location of the view. use bounds + center instead.
    @property(nonatomic) CGRect            frame;
    
    // use bounds/center and not frame if non-identity transform. if bounds dimension is odd, center may be have fractional part
    @property(nonatomic) CGRect            bounds;      // default bounds is zero origin, frame size. animatable
    @property(nonatomic) CGPoint           center;      // center is center of frame. animatable
    @property(nonatomic) CGAffineTransform transform;   // default is CGAffineTransformIdentity. animatable
    @property(nonatomic) CGFloat           contentScaleFactor NS_AVAILABLE_IOS(4_0);
    
    @property(nonatomic,getter=isMultipleTouchEnabled) BOOL multipleTouchEnabled __TVOS_PROHIBITED;   // default is NO
    @property(nonatomic,getter=isExclusiveTouch) BOOL       exclusiveTouch __TVOS_PROHIBITED;         // default is NO
    
    - (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
    
    - (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
    - (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
    - (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
    - (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;
    
    @property(nonatomic) BOOL               autoresizesSubviews; // default is YES. if set, subviews are adjusted according to their autoresizingMask if self.bounds changes
    @property(nonatomic) UIViewAutoresizing autoresizingMask;    // simple resize. default is UIViewAutoresizingNone
    
    - (CGSize)sizeThatFits:(CGSize)size;     // return 'best' size to fit given size. does not actually resize view. Default is return existing view size
    - (void)sizeToFit;                       // calls sizeThatFits: with current view bounds and changes bounds size.
    
    @end  
    

    ——————————hittest————————

    理解hittest就是理解响应链的核心点:

    iOS系统在判断哪个view来响应用户时 ,就是通过hittest在所有view的“树”上遍历:当前内存中所有view的子类都会执行hittest方法,逐个遍历每个叶子节点,如果叶子节点view上还有叶子,就进一步遍历下去。再啰嗦一句:即使某个view不会作为响应者,并且也不在点击范围内,只要它是当前显示的vc或者view的子视图,就会被httest遍历到,从而获取到是否可以作为响应者的信息。

    通过hittest的这个特点,我们便可以通过代码来验证一下,在我们所知的最顶层的view上重写hittest方法,通过断点获取到断点:
    我们验证的视图层次如下, vc上放着一个tableview,table上面一个yellowview:

    Table *table = [[Table alloc]initWithFrame:self.view.bounds];
     table.backgroundColor = [UIColor lightGrayColor];
    table.dataSource = self;
    table.delegate = self;
    [self.view addSubview: table];
        
    YellowView *view = [[YellowView alloc]initWithFrame:CGRectMake(20, 20, 200, 200)];
    view.backgroundColor = [UIColor yellowColor];
    [table addSubview:view];
    

    我们便可以在yellowview的实现中重写hittest

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
        return self;
    }
    

    得到堆栈如下:

    可以看出:
    序号25的栈帧上,主线程的runloop接收到UIEvent,然后经由分发器,首先分发到window上,然后传递给UIView,然后16--13正式遍历这个UIView的所有叶子的过程。在遍历过程中,如果子节点的任何一个view在执行hittest时返回了self(当前子view),就表示找到了响应的视图,这时父类也会通过返回值将这个子view向上返回,一直返回到window,返回到application,也就让系统知道了应该让谁来处理这个触摸。

    • 注意:以下三种情况下,view将不会执行hittest:
      hidden=YES
      userInteractionEnabled=NO
      alpha<0.01

    响应链

    通过上面的hittest遍历后,已经找到了响应者responder,由于其必然继承自UIResponder,我们可以重写UIResponder的touchBegan方法,touchBegan方法中再通过nextResponder属性来遍历地查看响应者链上的每个响应者:

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [super touchesBegan:touches withEvent:event];
        
        NSLog(@"------------------");
        NSLog(@"      v ");
        NSLog(@"    %@",  [self class]);
    
        UIResponder * next = [self nextResponder];
        
        while (next != nil)
        {
            NSLog(@"      v ");
            NSLog(@"    %@",  [next class]);
            next = [next nextResponder];
        }
        NSLog(@"------------------");
    }
    

    打印log结果如下图:


    我们以下图来具体描述下响应者链:



    UIView的nextResponder可能是UIView或UIViewViewController两种情况:

    • 当UIView的父类还是UIView时,nextResponder就是这个view父类;
    • 当UIView的父类还是UIViewViewController时,nextResponder就是这个vc父类;
      UIViewViewController的nextResponder是UIWindow,
      UIWindow的nextResponder是UIApplication的delegate(appDelegate)。

    手势与响应链关系

    这里不会讲述手势相关的知识, 只简单减少下手势相关问题调试时可能碰到的问题:当某个view添加了手势后,通过touchesBegan判断当前view是否是响应者时,并没有执行(实际这个view就是响应者),原因如下:


    当window接收到UIEvent后,并不会直接将UIEvent传递给链条上的view,因为其添加了手势,这时手势识别器会将处理UIEvent,如果识别出来是手势,则不会再调用touchesBegan;如果识别不出来是手势,才会调用touchesBegan,让view来处理这个UIEvent。

    应用

    通过响应链的基本知识,我们可以考虑下一些应用:

    1. 找到view所属vc

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

    可以看出这个方法正是利用了,view的nextResponder可能是view,如果是view就继续找nextResponder,直到是vc了,就是它所属的vc。如下左图。
    当情况更复杂一点,如下面右边图时:

    • 注意: 这时,vc上面还有childVC,这时childVC的nextResponder不是UIWindow,而是其父类vc,一直到vc已经是UIWindow的rootVC,其nextResponder才是UIWindow。

    2. 视图被遮挡时的响应

    当图A被图B遮挡时,通常B会将点击事件阻断,A收不到。这时如果想要让A能收到,可以有两种方式:

    • 利用hittest三种情况不调用的知识,可以设置A的userInteractionEnabled=NO;
    • 重写A的hittest方法,使其返回nil;

    相关文章

      网友评论

      本文标题:响应链的分析与应用

      本文链接:https://www.haomeiwen.com/subject/tivwottx.html