响应者链

1,293 阅读20分钟

先通过几个小问题来切入我们今天介绍的内容

1、如何扩大UIButton的响应范围?

创建一个自定义类JButton继承自UIButton并重写方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event{}

这里需要了解bounds属性,对bounds不了解可以看一下这两篇文章

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    CGFloat width = self.bounds.size.width+100;
    CGFloat height = self.bounds.size.height+100;
    // 向X轴正向扩大100,向Y正向扩大100
    CGRect bounds = CGRectMake(0, 0, width, height);
    return CGRectContainsPoint(bounds,point);
}

如果想向上、下、左、右各扩展50

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    CGFloat width = self.bounds.size.width+100;
    CGFloat height = self.bounds.size.height+100;
    // 向上、下、左、右各扩展50
    CGRect bounds = CGRectMake(-50, -50, width, height);
    return CGRectContainsPoint(bounds,point);
}

到这里我们知道了pointInside:withEvent方法是确定当前对象是否可以作为本次点击事件的响应者。

2、在父视图A上有一个子试图B,请问怎么实现点击B事件响应,但是点击A事件不响应?

我们在类A中重写方法

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *view = [super hitTest:point withEvent:event];
    if (view == self) {
        return nil;
    }
    return view;
}

也就是不让自己成为响应者,同理我也可以让自己成为响应者来处理事件,而不让子视图来处理

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

注意如果我在A类中重写

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    return NO;
}

父试图的pointInside:withEvent方法返回NO代表着父试图以及所有的子试图都不能作为响应者了。

3、如何实现一个事件多个对象相应?

我们创建自定义类AB,再创建A的子类SubAB的子类SubB。 创建SubA的实例对象作为父试图,创建SubB的实例对象作为子试图

    SubA *subA = [[SubA alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    subA.backgroundColor = [UIColor grayColor];
    [self.view addSubview:subA];

    SubB *subB = [[SubB alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    subB.backgroundColor = [UIColor redColor];
    [subA addSubview:subB];

如果想实现子类和父类同时响应事件

@implementation SubB
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"SubB");
    [super touchesBegan:touches withEvent:event];
}
@end


@implementation B
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"B");
}
@end

此时点击试图subB的打印结果

SubB
B

此时实现了子类SubB和父类B同时处理事件,这是从继承链来实现

如果想实现子试图和父试图同时响应事件

@implementation SubB
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"SubB");
    [[self superview] touchesBegan:touches withEvent:event];
}
@end


@implementation SubA
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"SubA");
}
@end

此时点击试图subB的打印结果

SubB
SubA

此时实现了子试图subB和父试图subA同时处理事件,这是从响应者链来实现 如果对super关键字不熟悉可以看一下这篇文章

响应者链

参考文章

通过前面几个小例子你可能已经懵了

  • pointInside:withEvent:是干啥的??
  • hitTest:withEvent:是干啥的??
  • 什么是继承链什么是响应者链 iOS中的事件有三大类型:
  • 触摸事件
  • 加速计事件
  • 远程控制事件 我们本文只讨论触摸事件。

系统响应阶段

  • 当手指触碰屏幕一个事件便产生了,首先处理这个事件的是IOKit.framework,他将触摸事件封装成IOHIDEvent对象
  • 通过mach portIOHIDEvent对象传递给SpringBoard进程,SpringBoard进程可以理解为桌面系统
  • 桌面系统经过一系列操作将事件传递给前台App进程的主线程(如果没有前台App,事件也可以由桌面系统等来处理,例如点击App图标启动一个应用程序),以上这些流程由系统来完成。

App响应阶段

  • App主线程的RunLoop默认注册了一些回调函数,其中就有用来接收触摸事件的回调函数
  • 桌面系统通过mach portIOHIDEvent事件传递给App主线程之后触发主线程RunLoopsource1函数回调,激活RunLoop
  • source1回调函数中又触发了source0回调函数,在这个函数中将IOHIDEvent对象封装成UIEvent对象
  • source0回调还将UIEvent对象添加到UIApplication对象的事件队列中等待被处理
  • UIApplication对象拿到UIEvent对象之后先进行了一步探测过程Hit-Testing确定哪些响应者可以处理这个事件,并确定最佳响应者,这个过程是自底向上的(从UIApplication->keyWindow->...->最上层的responder)
  • 确定了最佳响应者和响应者链之后就开始响应事件了,这个过程是自上而下的(和Hit-Testing顺序相反),当上层不处理时才会传递给下层响应者,如果直到UIApplication都不处理,那么这个事件就被放弃了,点了也白点,没有任何反应。

探测链和响应者链

通过上面的分析UIApplication拿到队列中的事件时首先进行的就是探测链Hit-Testing这一步要确定哪些响应者可以相应当前事件,主要涉及到两个方法

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

这两个方法我们可是不陌生,前面的几个小例子基本都是围绕这两个方法进行的,第一个方法调用第二个,如果第二个方法返回YES那么第一个方法就会返回当前试图对象作为响应者,这个过程是递归进行的,从UIWindow->...->上层响应者

16c3cd8383132762_tplv-t2oaga2asx-watermark.webp

Hit-Testing找到的最佳响应者拥有最先处理触摸事件的机会,如果不处理才会沿着响应者链进行传递,如果整个响应者链都没有响应者对事件做处理,那么时间就会被丢弃

16c3ce63324d2654_tplv-t2oaga2asx-watermark.webp

需要注意的是控制器和控制器的根试图都是响应者,如果根试图不处理才会传递给控制器进行处理。

代码分析

我们分析一下这几个类

  • UIEvent
  • UITouch
  • UIResponder
  • UIControl
UIResponder
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIResponder : NSObject <UIResponderStandardEditActions>
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
@property(nonatomic, readonly) BOOL canBecomeFirstResponder;    // default is NO
- (BOOL)becomeFirstResponder;
@property(nonatomic, readonly) BOOL canResignFirstResponder;    // default is YES
- (BOOL)resignFirstResponder;
@property(nonatomic, readonly) BOOL isFirstResponder;

// 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 API_AVAILABLE(ios(9.1));

......
@end

我们看到UIResponder中有nextResponder属性,说明可以生成响应者链,这里还有firstResponder相关的操作,可以确定最佳响应者,另外还有我们经常用到的touchBegan:withEvent:等操作方法

UIControl
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIControl : UIView
......
@property(nonatomic,getter=isEnabled) BOOL enabled;                                  // default is YES. if NO, ignores touch events and subclasses may draw differently
@property(nonatomic,getter=isSelected) BOOL selected;                                // default is NO may be used by some subclasses or by application
@property(nonatomic,getter=isHighlighted) BOOL highlighted;                          // default is NO. this gets set/cleared automatically when touch enters/exits during tracking and cleared on up
@property(nonatomic) UIControlContentVerticalAlignment contentVerticalAlignment;     // how to position content vertically inside control. default is center
@property(nonatomic) UIControlContentHorizontalAlignment contentHorizontalAlignment; // how to position content horizontally inside control. default is center
@property(nonatomic, readonly) UIControlContentHorizontalAlignment effectiveContentHorizontalAlignment; // how to position content horizontally inside control, guaranteed to return 'left' or 'right' for any 'leading' or 'trailing'

@property(nonatomic,readonly) UIControlState state;                  // could be more than one state (e.g. disabled|selected). synthesized from other flags.
@property(nonatomic,readonly,getter=isTracking) BOOL tracking;
@property(nonatomic,readonly,getter=isTouchInside) BOOL touchInside; // valid during tracking only

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event; // touch is sometimes nil if cancelTracking calls through to this.
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;   // event may be nil if cancelled for non-event reasons, e.g. removed from window

// add target/action for particular event. you can call this multiple times and you can specify multiple target/actions for a particular event.
// passing in nil as the target goes up the responder chain. The action may optionally include the sender and the event in that order
// the action cannot be NULL. Note that the target is not retained.
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
// remove the target/action for a set of events. pass in NULL for the action to remove all actions for that target
- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(UIControlEvents)controlEvents;
/// Adds the UIAction to a given event. UIActions are uniqued based on their identifier, and subsequent actions with the same identifier replace previously added actions. You may add multiple UIActions for corresponding controlEvents, and you may add the same action to multiple controlEvents.
- (void)addAction:(UIAction *)action forControlEvents:(UIControlEvents)controlEvents API_AVAILABLE(ios(14.0));
/// Removes the action from the set of passed control events.
- (void)removeAction:(UIAction *)action forControlEvents:(UIControlEvents)controlEvents API_AVAILABLE(ios(14.0));
/// Removes the action with the provided identifier from the set of passed control events.
- (void)removeActionForIdentifier:(UIActionIdentifier)actionIdentifier forControlEvents:(UIControlEvents)controlEvents API_AVAILABLE(ios(14.0));
// get info about target & actions. this makes it possible to enumerate all target/actions by checking for each event kind
@property(nonatomic,readonly) NSSet *allTargets;                                           // set may include NSNull to indicate at least one nil target
@property(nonatomic,readonly) UIControlEvents allControlEvents;                            // list of all events that have at least one action
- (nullable NSArray<NSString *> *)actionsForTarget:(nullable id)target forControlEvent:(UIControlEvents)controlEvent;    // single event. returns NSArray of NSString selector names. returns nil if none
......
@end

这里更多的是对状态的操作,例如选中状态、高亮状态、当前是否按下(touchInside)等等还有target-action相关操作,UIButton继承自UIControl

UIEvent
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIEvent : NSObject
@property(nonatomic,readonly) UIEventType     type API_AVAILABLE(ios(3.0));
@property(nonatomic,readonly) UIEventSubtype  subtype API_AVAILABLE(ios(3.0));
@property(nonatomic,readonly) NSTimeInterval  timestamp;
@property (nonatomic, readonly) UIKeyModifierFlags modifierFlags API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos);

@property (nonatomic, readonly) UIEventButtonMask buttonMask API_AVAILABLE(ios(13.4)) API_UNAVAILABLE(tvos, watchos);

@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture API_AVAILABLE(ios(3.2));

// An array of auxiliary UITouch’s for the touch events that did not get delivered for a given main touch. This also includes an auxiliary version of the main touch itself.
- (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));

// An array of auxiliary UITouch’s for touch events that are predicted to occur for a given main touch. These predictions may not exactly match the real behavior of the touch as it moves, so they should be interpreted as an estimate.
- (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));
@end

这里记录了状态type和时间timestamp,其余更多的都是对UITouch的操作了,例如在某个window上有多少个touch,在某个view上有多少个touch等等

UITouch
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval      timestamp;

@property(nonatomic,readonly) UITouchPhase        phase;

@property(nonatomic,readonly) NSUInteger          tapCount;   // touch down within a certain point within a certain amount of time

@property(nonatomic,readonly) UITouchType         type API_AVAILABLE(ios(9.0));
// majorRadius and majorRadiusTolerance are in points
// The majorRadius will be accurate +/- the majorRadiusTolerance
@property(nonatomic,readonly) CGFloat majorRadius API_AVAILABLE(ios(8.0));

@property(nonatomic,readonly) CGFloat majorRadiusTolerance API_AVAILABLE(ios(8.0));

@property(nullable,nonatomic,readonly,strong) UIWindow                        *window;
@property(nullable,nonatomic,readonly,strong) UIView                          *view;
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers API_AVAILABLE(ios(3.2));

- (CGPoint)locationInView:(nullable UIView *)view;
- (CGPoint)previousLocationInView:(nullable UIView *)view;

// Use these methods to gain additional precision that may be available from touches.
// Do not use precise locations for hit testing. A touch may hit test inside a view, yet have a precise location that lies just outside.
- (CGPoint)preciseLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));

- (CGPoint)precisePreviousLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));

// Force of the touch, where 1.0 represents the force of an average touch
@property(nonatomic,readonly) CGFloat force API_AVAILABLE(ios(9.0));

// Maximum possible force with this input mechanism
@property(nonatomic,readonly) CGFloat maximumPossibleForce API_AVAILABLE(ios(9.0));
@end

这里记录了点击事件、点击力度(3DTouch)、点击次数、坐标和一些坐标转化的方法,并记录了当前这次触摸是发生在哪个window和哪个view上的,例如我们可以通过locationInView方法和previousLocationInView方法做拖动操作。

小结

  • 探测链Hit-TestingUIWindow到末端响应者传递,确定最佳响应者,同时确定哪些响应者可以处理当前事件
  • 如果最佳响应者不处理对象,那么时间沿着响应链从末端响应者向UIWindow方向传递,如果整个链都没人处理那么事件丢弃。 关于事件机制到这里才刚刚开始,事件不止能被响应者处理,手势识别器UITapGestureRecognizertarget-action也可以捕获并处理事件

UIGestureRecognizer和UIControl

我这里简单了解下手势识别器和UIControl,如果想看更详细的介绍请移步

如果一个试图上添加了多个手势识别器,那么怎么控制他们的优先级?

// create a relationship with another gesture recognizer that will prevent this gesture's actions from being called until otherGestureRecognizer transitions to UIGestureRecognizerStateFailed
// if otherGestureRecognizer transitions to UIGestureRecognizerStateRecognized or UIGestureRecognizerStateBegan then this recognizer will instead transition to UIGestureRecognizerStateFailed
// example usage: a single tap may require a double tap to fail
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;

例如有两个手势识别器AB,如果执行

[A requireGestureRecognizerToFail:B];
  • B状态变为UIGestureRecognizerStateFailed之后A才可以开始识别
  • B状态变为UIGestureRecognizerStateRecognized或者UIGestureRecognizerStateBeganA状态变为UIGestureRecognizerStateFailed 例如单击手势要等到双击手势fail之后才开始识别,如果双击手势已经开始识别了,那么单击手势就不会识别。
// called once per attempt to recognize, so failure requirements can be determined lazily and may be set up between recognizers across view hierarchies
// return YES to set up a dynamic failure requirement between gestureRecognizer and otherGestureRecognizer
//
// note: returning YES is guaranteed to set up the failure requirement. returning NO does not guarantee that there will not be a failure requirement as the other gesture's counterpart delegate or subclass methods may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);

这两个方法也是用来设置手势识别器之间的依赖关系,和上面方法不同的是这两个方法是在每次尝试识别时调用一次,因此可以延迟确定失败要求,并且可以跨视图层次结构在识别器之间设置

注意:返回 YES 保证设置失败要求。返回 NO 并不能保证没有失败要求,因为其他手势的对应委托或子类方法可能返回 YES

// called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
// return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
//
// note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

gestureRecognizer的识别被otherGestureRecognizer阻止或者反过来otherGestureRecognizer的识别被gestureRecognizer阻止,返回 YES 以允许两者同时识别。默认实现返回 NO(默认情况下不能同时识别两个手势)

注意:返回 YES 保证允许同时识别。返回 NO 不能保证防止同时识别,因为另一个手势的代表可能返回 YES

通过这几个方法我们可以实现多个手势识别器之间的依赖关系。

给一个响应者添加手势识别器之后是执行手势识别器方法还是响应者自己的方法?

创建自定义类SubView

@implementation SubView

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"touchesBegan");
}

-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"touchesMoved");
}

-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"touchesEnded");
}

-(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"touchesCancelled");
}
@end

创建实例对象并添加手势识别器

    SubView *subview = [[SubView alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
    subview.backgroundColor = [UIColor redColor];
    [self.view addSubview:subview];

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
    [subview addGestureRecognizer:tap];

执行结果

touchesBegan
手势识别器方法
touchesCancelled

我们看到响应者方法中的touchesBegan执行了,当手势识别器识别事件以后响应者的touchesCancelled也执行了,我们下面进行分析

There may be times when you want a view to receive a touch before a gesture recognizer. But, before you can alter the delivery path of touches to views, you need to understand the default behavior. In the simple case, when a touch occurs, the touch object is passed from the UIApplication object to the UIWindow object. Then, the window first sends touches to any gesture recognizers attached the view where the touches occurred (or to that view’s superviews), before it passes the touch to the view object itself.

Gesture Recognizers Get the First Opportunity to Recognize a Touch

A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first. During the delay, if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.

16c484c30325cb3f_tplv-t2oaga2asx-watermark.webp

通过这段文字可以知道在事件响应阶段Window先将事件传递给手势识别器,然后再传递给响应者链。

假如探测过程Hit-Testing之后确定了响应者链A->B->CA、B、C分别添加了手势识别器gesA、gesB、gesC,那么此时同样记录了一个手势识别器之间的链式关系gesA->gesB->gesC,在事件响应阶段先将事件发送给这些手势识别器再发送给这些响应者,当手势识别器成功识别了手势以后就不会再向响应者发送事件,之前发送的事件也会被取消。所以上面例子中先执行了touchesBegan,手势识别器相应之后又执行了touchesCancelled

确定手势识别器是否可以识别的方法是UIGestureRecognizerDelegate中的

// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;

// called when a gesture recognizer attempts to transition out of UIGestureRecognizerStatePossible. returning NO causes it to transition to UIGestureRecognizerStateFailed
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

第一个方法是在 touchesBegan:withEvent: 之前调用,返回 NO 说明当前识别器不能处理事件,这个方法不会改变手势识别器状态。第二个方法是在手势识别器尝试识别事件的时候调用,如果返回NO当前手势识别器的状态就变成识别失败UIGestureRecognizerStateFailed

UIGestureRecognizer类中有以下几个属性

// default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to be sent to the view for all touches or presses recognized as part of this gesture immediately before the action method is called.
@property(nonatomic) BOOL cancelsTouchesInView;

这个属性用来控制当手势识别器识别成功之后是否取消响应者链对触摸事件的响应,默认是YES所以我们上面例子中手势识别器方法执行之后响应者对象执行了touchesCancelled,如果将这个属性改为NO那么touchesCancelled就不会执行

// default is NO.  causes all touch or press events to be delivered to the target view only after this gesture has failed recognition. set to YES to prevent views from processing any touches or presses that may be recognized as part of this gesture
@property(nonatomic) BOOL delaysTouchesBegan;

这个属性控制在确定手势识别器是否可以对事件进行处理的这段时间内是否将事件发送给响应者链,默认值是NO所以我们上面例子在手势识别器方法执行之前先执行了touchesBegan方法,如果设置为YES那么只有当手势识别器识别失败之后才会将事件发送给响应者链

// default is YES. causes touchesEnded or pressesEnded events to be delivered to the target view only after this gesture has failed recognition. this ensures that a touch or press that is part of the gesture can be cancelled if the gesture is recognized
@property(nonatomic) BOOL delaysTouchesEnded;

这个属性控制当手势识别器识别失败的情况下调用touchesEnded的时机

如果给UIButton添加一个手势识别器那么执行按钮方法还是手势识别器方法?

    SubView *subview = [[SubView alloc] initWithFrame:CGRectMake(100, 100, 300, 200)];
    subview.backgroundColor = [UIColor redColor];
    [self.view addSubview:subview];

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
    [subview addGestureRecognizer:tap];
    
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    btn.frame = CGRectMake(0, 0, 200, 100);
    btn.backgroundColor = [UIColor greenColor];
    [btn addTarget:self action:@selector(btnDidClicked:) forControlEvents:UIControlEventTouchUpInside];
    [subview addSubview:btn];

我们来分析一下UIButton的继承链,UIButton->UIControl->UIView->UIResponder,那么UIButton属于响应者链对象,此时是不是应该先将事件发送给手势识别器呢???

执行结果

btnDidClicked:

响应了UIButton通过target-action设置的方法,没有响应父试图的手势识别器方法

Interacting with Other User Interface Controls

In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer. This applies only to gesture recognition that overlaps the default action for a control, which includes:

  • A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
  • A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
  • A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.

If you have a custom subclass of one of these controls and you want to change the default action, attach a gesture recognizer directly to the control instead of to the parent view. Then, the gesture recognizer receives the touch event first. As always, be sure to read the iOS Human Interface Guidelines to ensure that your app offers an intuitive user experience, especially when overriding the default behavior of a standard control.

对于UIButtonUISwitchUIStepperUISegmentedControlUIPageControl部分UIControl来说,注意不是全部的,为了防止UIControl的默认手势和父试图的手势识别器方法冲突,UIControl最后会响应触摸事件,如果想要手势识别器来处理事件,那么可以给UIControl对象添加手势识别器。

上面例子中我们给btn添加一个手势识别器

    UITapGestureRecognizer *btnTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(btnTap:)];
    [btn addGestureRecognizer:btnTap];

那么点击按钮的时候执行结果是

btnTap:

执行了UIButton的手势识别器方法。

通过前面分析我们知道,在没有手势识别器时事件可以沿着响应者链传递,有手势识别器的时候事件会先传递给手势识别器们进行识别,那么这里既没有沿着响应者链传递也没有被手势识别器识别,UIButton是如何实现的呢,我们分别来解释

  • 第一个问题:UIButton是如何实现让父试图的手势识别器不识别事件呢??

Subclasses may override this method and use it to prevent the recognition of particular gestures. For example, the UISlider class uses this method to prevent swipes parallel to the slider’s travel direction and that start in the thumb.

At the time this method is called, the gesture recognizer is in the UIGestureRecognizerStatePossible state and thinks it has the events needed to move to the UIGestureRecognizerStateBegan state.

The default implementation of this method returns YES.

UIButton是通过重写gestureRecognizerShouldBegin:方法来实现的。通过上面分析我们知道这个方法是在手势开始识别的时候执行的并且可以跨试图设置,如果识别失败的话手势识别器的状态也会变成UIGestureRecognizerStateFailedUIButton通过这个方法将父试图的手势识别器们都设置成识别失败,而UIButton自己的手势识别器和子试图的手势识别器不受影响

  • 第二个问题:UIButton是如何实现让事件没有沿着响应者链传递的呢?? 这是UIButton通过重写touchesBegan:withEvent:方法截断了事件在响应者链继续传递,此时会执行通过target-action机制设置的方法,如果target为空事件才会沿着响应者链继续传递。

验证一下,我们自定义一个UIButton的子类JButton

@implementation JButton
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"自定义按钮的touchesBegan:");
    // 这里不实现super方法
}
@end

创建实例对象

    JButton *btn = [JButton buttonWithType:UIButtonTypeSystem];
    btn.frame = CGRectMake(100, 100, 200, 100);
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action:@selector(btnDidClicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];

点击按钮执行结果为

自定义按钮的touchesBegan:

此时执行了touchesBegan方法但是没有执行action方法。

小结

  • 在探测阶段Hit-Testing不仅确定了最佳响应者和响应者链,如果有手势识别器的话还确定了哪些手势识别器可以识别当前事件
  • 在响应阶段如果没有手势识别器也没有target-action模式添加响应方法,那么最佳响应者优先处理事件,如果没有处理则沿着响应者链进行传递,直到UIApplication如果都不响应则丢弃本次事件。
  • 在响应阶段如果有手势识别器那么时间先发送给手势识别器进行识别,然后再发送给响应者链。
  • 如果响应者链中有部分UIControl对象,例如UIButton(自定义的不行哦),那么优先执行UIControl对象通过target-action设置的方法,如果target为空才会沿着响应者链进行传递。

参考文章