Let’s start with a couple of quick questions to get to what we’re talking about today

1, how to expandUIButtonResponse range?

Create a custom class JButton that inherits the UIButton override method

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

You need to know the bounds property here, but if you don’t know bounds, take a look at these two articles

  • # The difference between frame and bounds
  • # Difference between frame and bounds

If the width and height of the button is (100,100), and you want the click area of the button to expand by 100 in the X-axis and 100 in the Y-axis, you can do this

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{ CGFloat width = self.bounds.size.width+100; CGFloat height = self.bounds.size.height+100; CGRect bounds = CGRectMake(0, 0, width, height); return CGRectContainsPoint(bounds,point); }Copy the code

If you want to go up, down, left, and right by 50

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{ CGFloat width = self.bounds.size.width+100; CGFloat height = self.bounds.size.height+100; CGRect bounds = CGRectMake(-50, -50, width, height); return CGRectContainsPoint(bounds,point); }Copy the code

Here we know that the pointInside:withEvent method determines whether the current object can be a responder to the click event.

2. In the superviewAThere’s a kid trying toB, how to achieve clickBEvent response, but clickAEvent not responding?

We override the method in class A

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

So instead of making myself a responder, I can make myself a responder to handle events instead of letting my subview handle them

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

Notice if I rewrite it in class A

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

The pointInside of the parent attempt :withEvent method returns NO indicating that the parent attempt and all child attempts are NO longer responders.

3, how to achieve an event multiple object corresponding?

We create custom classes A and B, subclass A and subclass B. Create instance object of SubA as parent attempt and create instance object of SubB as child attempt

    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];
Copy the code

If you want to implement both subclasses and superclasses to respond to events

@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
Copy the code

At this point, click the print result of attempting subB

SubB
B
Copy the code

At this point, subclass B and superclass B are implemented to handle events simultaneously from the inheritance chain

If you want to implement both the child attempt and the parent attempt to respond to the event

@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");  } @endCopy the code

At this point, click the print result of attempting subB

SubB
SubA
Copy the code

Now we implement both child attempt subB and parent attempt subA to handle events at the same time, and this is implemented from the responder chain if you’re not familiar with the super keyword read this article

  • # OC底层之self、super

The responder chain

Refer to the article

  • # The most detailed iOS event delivery and response mechanism ever – Principles
  • # In-depth understanding of iOS event mechanics

The responder chain part is basically summarized from these two articles. If you are interested, you can read the original text.

You might be confused by the first few small examples

  • pointInside:withEvent:What does it do?
  • hitTest:withEvent:What does it do?
  • What is theInheritance chainWhat is theThe responder chain

There are three main types of events in iOS:

  • Touch events
  • Accelerometer event
  • Remote control event

We will only discuss touch events in this article.

System response stage

  • When a finger touches the screen an event is generated, and the first thing to handle that event isIOKit.framework, he encapsulates the touch event asIOHIDEventobject
  • throughmach portwillIOHIDEventObject passed toSpringBoardThe process,SpringBoardProcesses can be understood as desktop systems
  • The desktop system passes events to the foreground through a series of operationsAppThe main thread of the process (if there is no foreground)App, events can also be handled by desktop systems, such as clicksAppIcon to start an application), the above process is done by the system.

App response stage

  • AppThe main thread of theRunLoopSome callbacks are registered by default, including one that receives touch events
  • Desktop System Passmach portwillIOHIDEventEvent pass toAppTriggers the main thread after the main threadRunLoopthesource1Function callback, activatedRunLoop
  • insource1The callback is triggered againsource0The callback function, in which theIOHIDEventObject encapsulated asUIEventobject
  • source0The callback will beUIEventObject added toUIApplicationObject waiting to be processed
  • UIApplicationObject toUIEventObject is then first probedHit-TestingDetermine which responders can handle the event, and determineBest responders, the process is bottom-up (fromUIApplication->keyWindow– >… -> Top levelresponder)
  • To determine theBest respondersAnd responder chains begin to respond to events, and the process is top-down (andHit-TestingReverse order), is passed to the lower responder when the upper layer does not process, if untilUIApplicationDo not handle, then the event is abandoned, click also blank, no reaction.

Probe chain and responder chain

Through the above analysis, when UIApplication gets events in the queue, the first step is to detect chain hit-testing. This step is to determine which responders can respond to the current event, which mainly involves two methods

// 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;   
Copy the code

The first method calls the second method, and if the second method returns YES, the first method returns the current attempt object as the responder. This process is recursive, from UIWindow->… -> Upper responder

The best responder found by hit-testing has the first chance to process the touch event. If it does not, it will be passed along the responder chain. If there is no responder in the entire responder chain to process the event, the time will be discarded

Note that both the controller and the controller’s root attempt are responders. If the root attempt is not processed, it is passed to the controller for processing.

The code analysis

Let’s analyze these classes

  • 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)); . @endCopy the code

We see that there’s a nextResponder property in UIResponder, which means you can generate a chain of responders, and there’s a firstResponder operation that determines the best responder, and there’s a touchBegan withEvent that we use all the time

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 ...... @endCopy the code

There’s a lot more manipulation of states, like selected state, highlighted state, whether it’s currently down or not, and target-action, UIButton inherits from 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 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 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)); @endCopy the code

This is where the status type and timestamp are recorded, and the rest is more about UITouch operations, such as how many touches there are on a window, how many touches there are on a view, and so on

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)); @endCopy the code

It records click events, click dynamics (3DTouch), click times, coordinates and some coordinate conversion methods, and records which window and which view the current touch took place on. So, for example, we can drag through the locationInView method and the previousLocationInView method.

summary

  • Detection of chainHit-TestingfromUIWindowPass to the end responder to determine the best responder and which responders can process the current object
  • If the best responder does not process the object, then time moves along the response chain from the end responder to the end responderUIWindowDirection passing, if the whole chain is not handled then the event is discarded.

This is just the beginning of the event mechanism. Not only can events be handled by responders, but gesture recognizers UITapGestureRecognizer and target-Action can also capture and handle events

Refer to the article

  • # UIButton (UIButton) click range (optional direction extension oh)
  • # The difference between frame and bounds
  • # Difference between frame and bounds
  • # OC底层之self、super
  • # The most detailed iOS event delivery and response mechanism ever – Principles
  • # In-depth understanding of iOS event mechanics