This article belongs to “Jane Book — Liu Xiaozhuang” original, please note:

< Jane books – Liu Xiaozhuang > https://www.jianshu.com/p/b0884faae603


I haven’t written a blog for a long time, almost a year. During this period, my blog has not changed all the time. If you are careful, you should notice that I have been replying to the questions in the comment section and private messages and updating several previous blogs.

Last year was a meaningful year, and I learned a lot from all aspects, not just the technical aspects. Many people write year-end summary, I am more lazy do not write, heart self-summary, ha ha 😀.

Getting back to the point, it’s common to see gestures or click-throughs in projects, which are part of response event handling. But a lot of people don’t know much about responding to events in iOS, and they often run into gestures that clash, events that don’t respond, and so on, so they check blogs. However, many blogs are not very complete or of low quality. I have spent some time to write out what I have learned and understood about iOS event handling for your reference.


UIResponder

UIResponder is an iOS API for handling user events. It can handle touch events, press events (3D touch), remote control events, hardware movement events. Can through the touchesBegan, pressesBegan, motionBegan remoteControlReceivedWithEvent, get back to the corresponding message. UIResponder is used not only to receive events, but also to process and pass corresponding events, and if the current responder can’t handle it, forward it to another appropriate responder for processing.

Applications receive and handle events through responders, which can be any subclass that inherits from UIResponder, such as UIView, UIViewController, UIApplication, and so on. When the event arrives, the system passes the event to the appropriate responder and makes it the first responder.

Events that are not handled by the first responder will be passed in the responder chain, and the rule of delivery is determined by the nextResponder of UIResponder. You can override this property to determine the rule of delivery. When an event arrives, the first responder does not receive the message and passes it back down the responder chain.

Find the first responder

Based on the API

There are two key apis for finding the first responder, and finding the first responder is done by constantly calling those apis of the subview.

Method is called to get the clicked view, which is the first responder.

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

HitTest :withEvent: this method is called internally to determine whether the clicked area is on the view, and returns YES if it is, and NO if it is not.

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

Find the first responder

When the application receives the event, it hands it tokeyWindowThe root view traverses the sub-views step by step according to the view hierarchy, and continuously judges the view scope during the traversal, and finally finds the first responder.

Starting with the keyWindow, we iterate through the subviews, calling UIView’s hitTest:withEvent: method, looking for the view in the clicked region, and calling the hitTest:withEvent: method of the subview that returns the view, and so on. If the subview is not in the click area or there is no subview, the current view is the first responder.

In the hitTest:withEvent: method, the subview is traversed from top to bottom and subViews’ pointInside:withEvent: method is called to find the topmost subview within the click area. If a subview is found, its hitTest:withEvent: method is called, and the process continues, and so on. If the subview is not in the click area, ignore the view and its subviews and continue traversing the other views.

You can override the corresponding method to control the traversal process. Make your own judgment by overriding the pointInside:withEvent: method and return YES or NO to see if the clicked area is on the view. Return the clicked view by overriding the hitTest:withEvent: method.

When traversing a view, this method ignores views in any of the following three cases, or if the view has any of the following characteristics. However, the background color of the view is clearColor, which is not ignored.

  1. The view ofhiddenEqual to YES.
  2. The view ofalphaLess than or equal to 0.01.
  3. The view ofuserInteractionEnabledTo NO.

If the click event occurs outside the view but inside its child view, the child view cannot receive the event and become the first responder. This is because it is ignored during the hitTest:withEvent: of its parent view.

events

Delivery process

  1. UIApplicationReceives the event and passes the event tokeyWindow.
  2. keyWindowtraversesubViewsthehitTest:withEvent:Method to find the appropriate view within the click area to handle the event.
  3. UIViewIt is also traversed by the child view ofsubViewsthehitTest:withEvent:Methods, and so on.
  4. Until you find the top view in the click area, step the view back toUIApplication.
  5. In the search for the first responder, a responder chain has been formed.
  6. The application calls the first responder to handle the event first.
  7. If the first responder cannot handle the event, it is callednextResponderMethod, always looking for an object in the responder chain that can handle the event.
  8. The last toUIApplicationIf no object can process the event, the event is discarded.

Simulation code

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {
        return nil;
    }
    
    BOOL inside = [self pointInside:point withEvent:event];
    if (inside) {
        NSArray *subViews = self.subviews;
        // Search from top to bottom for subviews
        for (NSInteger i = subViews.count - 1; i >= 0; i--) {
            UIView *subView = subViews[i];
            CGPoint insidePoint = [self convertPoint:point toView:subView];
            UIView *hitView = [subView hitTest:insidePoint withEvent:event];
            if (hitView) {
                returnhitView; }}return self;
    }
    return nil;
}
Copy the code

The sample

As shown in the figure above, the responder chain is as follows:

  1. If you clickUITextFieldThen it will be the first responder.
  2. iftextFieldAn unprocessed event is passed to the next level of the responder chain, which is its parent view.
  3. The event is not handled by the superview and is passed down, i.eUIViewControllertheView.
  4. If the controller’sViewUnprocessed events are handed to the controller for processing.
  5. If the controller does not process it, it will be handedUIWindow.
  6. And then it will be handed overUIApplication.
  7. The last toUIApplicationDelegateIf it is not processed, the event is discarded.

The event is passed through the UITouch. When the event arrives, the first responder assigns the corresponding UITouch. The UITouch stays with the first responder and changes according to the current event.

UIViewController has no hitTest:withEvent: method, so the controller is not involved in finding the response view. But the controller is in the responder chain, and if the controller’s View doesn’t handle the event, it’s handed over to the controller. If the controller doesn’t handle it, it passes it on to the View’s next responder.

Pay attention to

  1. In the implementationhitTest:withEvent:Method if the view ishiddenThe three ignored cases equal to NO are returnednil.
  2. If the current view is in the responder chain, but it does not handle events, its sibling views are not considered, even if both sibling views are in the click range.
  3. UIImageViewtheuserInteractionEnabledDefault is NO if you wantUIImageViewRespond to interactive events by setting the property to YES.

Incident control

Events to intercept

Sometimes you want a specified view to respond to an event without passing events to its children by overriding the hitTest:withEvent: method. After the method is executed, the view is returned instead of continuing through the subviews, so that the end of the responder chain is the current view.

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

Event forwarding

During development, it is common for a child view to be displayed outside of the parent view, so you can override the pointInside:withEvent: method of that view to expand the click area to cover all child views.

Given the view structure above, the SuperView Subview is outside the scope of its view. If you click the part of the Subview outside the SuperView, it will not respond to events. So by overwriting the pointInside:withEvent: method and enlarging the response area to a dashed area containing all of the SuperView’s children, you can make the children respond to events.

Events are passed level by level

If you want each level of UIResponder in the responder chain to respond to events, you can create scenes in each level of UIResponder and call the super method.

Except it doesn’t include UIControl subclasses and UIGestureRecognizer subclasses, which break the responder chain directly.

Gesture Recognizer

If the view has an additional gesture recognizer when an event arrives, the gesture recognizer takes precedence over the event. If the gesture recognizer does not process the event, it passes it to the view for processing, and if it does not, the view passes it back down the responder chain.

The ‘touches’ method sometimes fails when the’ touches’ method is implemented and the ‘touches’ gesture is added when the’ touches’ chain is present at the same time, because the ‘touches’ take precedence over the’ touches’ chain.

The first responder is found using the hitTest and pointInside operations that are performed upon the arrival of the event, as described in detail above. When the first responder is found and returned to UIApplication, UIApplication sends the event to the first responder and traverses the entire responder chain. If there is a gesture in the responder chain that handles the current event, hand the event to the gesture and call cancelled on Touches to cancel the responder chain.

When UIApplication sends an event to the first responder and traverses the responder chain looking for gestures, it starts implementing the Touches methods in the responder chain. The touchesBegan and touchesMoved methods will be executed first. If the responder chain can continue to respond to the event, the ‘touchesEnded’ method will be executed to indicate that the event is complete. If the event is handed over to the gesture, the ‘touchesCancelled’ method will be called to interrupt the responder chain.

According to Apple’s official documentation, gestures do not participate in the responder chain delivery event, but do use hitTest to find the view of the response. Gestures and responder chains need hitTest to determine the responder chain. When UIApplication sends a message to the responder chain, the gesture responds to the event as long as there is a gesture that can process the event in the responder chain. If the gesture is not in the responder chain, the event cannot be processed.

Apple UIGestureRecognizer Documentation

UIControl

Based on the above rules for handling gestures and responder chains, we can see that controls such as UIButton or UISlider do not comply with this rule. UIButton can respond to events normally even if its parent view has added tapGestureRecognizer, and tap gestures do not respond.

Take UIButton for example, UIButton also finds the first responder by hitTest. The difference is that if UIButton is the first Responder, then UIApplication sends the event directly, not through the Responder Chain. If it cannot handle the event, it is passed to the gesture handler or responder chain.

It’s not just UIButton that sends events directly from UIApplication, all classes that inherit from UIControl, they send events directly from UIApplication.

Apple UIControl Documentation

Event delivery priority

test

To make informed inferences about the implementation and delivery mechanism of response events, we do the following tests.

Example 1

Assuming that the RootView, SuperView, and Button implement touches, and that Button adds the buttonAction: action, the call is created as follows.

RootView -> hitTest:withEvent:
RootView -> pointInside:withEvent:
SuperView -> hitTest:withEvent:
SuperView -> pointInside:withEvent:
Button -> hitTest:withEvent:
Button -> pointInside:withEvent:
RootView -> hitTest:withEvent:
RootView -> pointInside:withEvent:

Button -> touchesBegan:withEvent:
Button -> touchesEnded:withEvent:
Button -> buttonAction:
Copy the code
Example 2

Using the same view structure, we add the UITapGestureRecognizer gesture to the RootView and receive the callback through the tapAction: method. After clicking the SuperView above, the method is called as follows.

RootView -> hitTest:withEvent:
RootView -> pointInside:withEvent:
SuperView -> hitTest:withEvent:
SuperView -> pointInside:withEvent:
Button -> hitTest:withEvent:
Button -> pointInside:withEvent:
RootView -> hitTest:withEvent:
RootView -> pointInside:withEvent:

RootView -> gestureRecognizer:shouldReceivePress:
RootView -> gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:
SuperView -> touchesBegan:withEvent:
RootView -> gestureRecognizerShouldBegin:
RootView -> tapAction:
SuperView -> touchesCancelled:
Copy the code
Example 3

Subview1, Subview2, and Subview3 are sibling views, all subviews of SuperView. We add the UITapGestureRecognizer gesture to Subview1 and receive the callback through the subView1Action: method. After clicking Subview3 above, the method call looks like this.

SuperView -> hitTest:withEvent:
SuperView -> pointInside:withEvent:
Subview3 -> hitTest:withEvent:
Subview3 -> pointInside:withEvent:
SuperView -> hitTest:withEvent:
SuperView -> pointInside:withEvent:

Subview3 -> touchesBegan:withEvent:
Subview3 -> touchesEnded:withEvent:
Copy the code

From the above example, although Subview1 is below Subview3 and the gesture is added, the click area is on both the Subview1 and Subview3 views. But since there is no Subview1 in the responder chain after hitTest and pointInside, the gesture of Subview1 is not responded to.

Analysis of the

Based on our tests above, infer the priority of iOS response events, as well as the overall response logic.

When the event arrives, the view of the first responder is found by looking up the view from the Window using the hitTest and pointInside methods. Once the first responder is found, the system determines whether it inherits from UIControl or UIResponder, and if it inherits from UIControl, it sends messages directly to it through UIApplication, and it doesn’t send messages to the responder chain anymore.

If it’s a class that inherits from UIResponder, the first responder’s touchesBegin is called, and instead of immediately executing ‘touchesEnded,’ the call is followed by a lookup back up the responder chain. If a view in the responder chain adds a gesture, it goes to the proxy method of the gesture. If the proxy method returns that it can respond to the event, the first responder’s event is canceled and its touchesCanceled method is called. The gesture then responds to the event.

If the gesture does not handle the event, hand it over to the first responder. If the first responder also cannot respond to the event, it continues down the responder chain until it finds a UIResponder object that can handle the event. If the UIApplication is found with no object responding to the event, the event is discarded.

Receive in-depth profiling of events

inUIApplicationBefore the response event is received, there is more complex system-level processing, which is roughly as follows.

  1. The system uses IOKit. Framework to handle hardware operations, in which screen processing is also done by IOKit (IOKit may be registered to listen to the screen output port). When the user operates on the screen, IOKit receives the screen operation and encapsulates the operation as an IOHIDEvent object. Events are forwarded to SpringBoard for processing through a Mach port(IPC interprocess communication).

  2. SpringBoard is a desktop application for iOS. The SpringBoard receives an event from the Mach port and wakes up the main Runloop to process it. Main runloop events to source1, source1 invokes __IOHIDEventSystemClientQueueCallback () function.

  3. The function internally determines if any programs are displayed in the foreground, and if so it forwards IOHIDEvent events to the program via a Mach port. If no application is displayed in the foreground, SpringBoard’s desktop application is displayed in the foreground, that is, the user acted on the desktop. Events to source0 __IOHIDEventSystemClientQueueCallback () function will handle, source0 invokes __UIApplicationHandleEventQueue () function, function inside can make specific processing operations.

  4. For example, when a user clicks an icon of an application, the application will be launched. Applications to receive messages from the SpringBoard, awakens the main runloop and gave the news to source1, source1 call __IOHIDEventSystemClientQueueCallback () function, Inside the function will be the event to the source0 processing, and invoke the source0 __UIApplicationHandleEventQueue () function. In __UIApplicationHandleEventQueue () function, will convert the IOHIDEvent passed UIEvent object.

  5. Inside the function, the sendEvent: method of UIApplication is called to pass the UIEvent to the first responder or UIControl object for processing, which contains several UITouch objects inside the UIEvent.

Tips

Source1 is used by Runloop to handle system events from the Mach port, and source0 is used to handle user events. When Source1 receives system events, it calls Source0’s functions, so ultimately these events are handled by Source0.

tip

In development, sometimes there will be a need to find the controller corresponding to the current View, this time can use what we learned above, according to the responder chain to find the nearest controller.

The nextResponder method is provided in UIResponder to find the next-level response object for the current response link. You can start from the current UIView and keep calling the nextResponder, looking for the object in the nextResponder chain, and you can find the UIViewController closest to you.

Sample code:

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