IOS event handling, it’s enough for me~

Time:2020-2-27
This article belongs to the original of “Jianshu Liu Xiaozhuang”. Please indicate:

< Jianshu Liu Xiaozhuang > https://www.jianshu.com/p/b0884faae603


I haven’t written blog for a long time. It’s just a year before and after. During this period, the blog has not been unchanged. Careful students should be able to find that I have been replying to comments and private letters, and updating several previous blogs.

Last year was a meaningful year. I have learned a lot from all aspects, not limited to technology. Many people write year-end summaries. I’m lazy, so I don’t write them. Let’s summarize myself, ha ha?.

Back to the main point, in the project often encounter a variety of gestures or click event processing and so on, which belong to response event processing. But many people don’t know how to deal with response events in IOS. They often encounter problems such as gesture conflict and event non response, so they go to check the blog.
But now many blogs are not very complete, or the quality is not high. I take the time to write the IOS event processing that I have learned and understood in these two days for your reference.


IOS event handling, it's enough for me~

UIResponder

UIResponderIs the API used to handle user events in IOS, which can handle touch events and press events(3D touch), remote control events, hardware motion events.Can passtouchesBeganpressesBeganmotionBeganremoteControlReceivedWithEventAnd get the corresponding callback message.UIResponderIt is not only used to receive events, but also to process and deliver corresponding events. If the current responder cannot handle it, it will be forwarded to other appropriate responders for processing.

Applications receive and process events through responders, which can be inherited fromUIResponderAny subclass of, for exampleUIViewUIViewControllerUIApplicationAnd so on. When an 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. The delivery rules are as follows:UIResponderOfnextResponderDecision, you can override this property to determine the delivery rule. When an event arrives and the first responder does not receive the message, it is passed back along the responder chain.

Find first responder

Foundation API

When looking for the first responder, there are two very criticalAPI, find the first responder by calling these two subviews continuouslyAPIDone.

Call the method to get the clicked view, which is the first responder.

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

hitTest:withEvent:This method will be called internally to determine whether the click area is on the view. If yes, returnYES, return if notNO

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

Find first responder

When the application receives the event, it hands it over tokeyWindowAnd forward it to the root view. The root view traverses the subviews step by step according to the view level. In the process of traversal, the view range is constantly judged and the first responder is finally found.

fromkeyWindowStart, traverse the subview step by step, and call again and againUIViewOfhitTest:withEvent:Method, find the view in the clicked area, and continue to call thehitTest:withEvent:Method, and so on. If the subview is not in the click area or there is no subview, the current view is the first responder.

stayhitTest:withEvent:Method, traverses the subview from top to bottom, and callssubViewsOfpointInside:withEvent:Method to find the top subview in the click area. Call subview if foundhitTest:withEvent:Method and continue the process, and so on. If the subview is not in the click area, ignore this view and its subviews, and continue to traverse other views.

This traversal can be controlled by overriding the corresponding method. By rewritingpointInside:withEvent:Method to make your own judgment and returnYESorNO, to return whether the click area is on the view. By rewritinghitTest:withEvent:Method to return to the clicked view.

This method ignores the following three views when traversing the view, and ignores the view if it has the following characteristics. But the background color of the view isclearColor, is not in the ignored range.

  1. ViewhiddenEqual to YES.
  2. ViewalphaLess than or equal to 0.01.
  3. ViewuserInteractionEnabledIt’s NO.

If the click event occurs outside the view, but inside its sub view, the sub view cannot receive the event and become the first responder. This is because in its parent viewhitTest:withEvent:In the process, it will be ignored.

Event transmission

Transfer process

  1. UIApplicationReceive event, pass event tokeyWindow
  2. keyWindowergodicsubViewsOfhitTest:withEvent:Method, find the appropriate view in the click area to handle the event.
  3. UIViewSubviews of will also traverse itssubViewsOfhitTest:withEvent:Method, and so on.
  4. Until you find the top view in the click area, return the view to theUIApplication
  5. In the process of finding the first responder, a chain of responders has been formed.
  6. The application first calls the first responder to handle the event.
  7. If the first responder cannot process the event, it is callednextResponderMethod, always find the object in the responder chain that can handle the event.
  8. Finally arriveUIApplicationIf there is still no object that can handle 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;
        //Look up and down 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) {
                return hitView;
            }
        }
        return self;
    }
    return nil;
}

Example

IOS event handling, it's enough for me~

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

  1. If ClickUITextFieldThen it becomes the first responder.
  2. IftextFieldUnhandled events are passed to the next responder chain, the parent view.
  3. The unhandled events in the parent view continue to be passed down, that isUIViewControllerOfView
  4. If theViewIf the event is not processed, it will be handed to the controller for processing.
  5. If the controller is not handled, it will be handed over toUIWindow
  6. And then I’ll give it to youUIApplication
  7. Finally handed over toUIApplicationDelegate, and discard the event if it is not processed.

Event passingUITouchWhen the event arrives, the first responder will assign the correspondingUITouchUITouchWill follow the first responder all the time and change according to the current eventUITouchIt will also change when the event endsUITouchReleased.

UIViewControllerNo,hitTest:withEvent:Method, so the controller does not participate in the process of finding the response view. But the controller is in the responder chain, if the controller’sViewDo not handle the event, it will be handed over to the controller. If the controller doesn’t handle it, give it toViewThe next responder processing for.

Be careful

  1. In executionhitTest:withEvent:Method, if the view ishiddenIf the three cases equal to no are ignored, the view change will returnnil
  2. If the current view is in the responder chain, but it does not handle events, its sibling view is not considered, even if its sibling view and both are in the click range.
  3. UIImageViewOfuserInteractionEnabledDefault to no, if you wantUIImageViewRespond to interactive events by setting the property to yes.

Event control

Event interception

Sometimes, you can override thehitTest:withEvent:Method. After the method is executed, the view is returned directly instead of traversing the subview, so the terminal of the responder chain is the current view.

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

Event Forwarding

In the development process, it is often encountered that the display range of the child view exceeds that of the parent view. At this time, you can override thepointInside:withEvent:Method, expand the click area to cover all subviews.

IOS event handling, it's enough for me~

Suppose you have the above view structure,SuperViewOfSubviewBeyond its view range, if you clickSubviewThe part outside the parent view cannot respond to events. So by rewritingpointInside:withEvent:Method, expanding the response area to a dotted area, containingSuperViewAll subviews of the to have subviews respond to events.

Event level by level transmission

If you want every level in the responder chainUIResponderCan respond to events at each levelUIResponderRealization in both citiestouchesAnd callsuperMethod, the responder chain events can be passed level by level.

But it doesn’t includeUIControlSubclasses andUIGestureRecognizerThese two classes directly break the responder chain.

Gesture Recognizer

If there is an additional gesture recognizer in the view when an event arrives, the gesture recognizer takes priority in handling the event. If the gesture recognizer does not handle the event, the event is handed over to the view for processing. If the view is not processed, it continues to pass backward along the responder chain.

IOS event handling, it's enough for me~

When the responder chain and gesture appear at the same time, it is realizedtouchesMethod added gesture again, you will findtouchesMethods sometimes fail because the execution priority of the gesture is higher than that of the responder chain.

The event will be executed firsthitTestandpointInsideOperation, find the first responder through these two methods, which has been described in detail above. When the first responder is found and returned toUIApplicationLater,UIApplicationEvents are dispatched to the first responder and the entire responder chain is traversed. If the gesture in the responder chain can handle the current event, the event is handed over to the gesture processing, and thetouchesOfcalcelledMethod to cancel the responder chain.

stayUIApplicationWhen the event is dispatched to the first responder and the gesture is searched by traversing the responder chain, thetouchesSeries method. It will be executed first.touchesBeganandtouchesMovedMethod, if the responder chain can continue to respond to the event, executetouchesEndedMethod indicates that the event is completed. If the event is handed over to gesture processing, calltouchesCancelledMethod breaks the responder chain.

According to Apple’s official documents, gestures do not participate in responder chain delivery events, but also throughhitTestTo find the response view, gesture and responder chain need to passhitTestMethod to determine the responder chain. stayUIApplicationWhen sending a message to the responder chain, as long as there is a gesture in the responder chain that can handle the event, the gesture responds to the event. If the gesture is not in the responder chain, the event cannot be processed.

Apple UIGestureRecognizer Documentation

UIControl

According to the above rules of gesture and responder chain processing, we will find thatUIButtonperhapsUISliderControl, does not conform to this processing rule.UIButtonCan be added in its parent viewtapGestureRecognizerIn the case of, still respond to the event normally, andtapGesture does not respond.

IOS event handling, it's enough for me~

withUIButtonFor example,UIButtonAlso throughhitTestOf the first responder. The difference is that ifUIButtonIs the first responder, then theUIApplicationDispatch incident, failResponder ChainDistribute. If it cannot handle the event, it is passed to the gesture handler or responder chain.

Not onlyUIButtonIs directly fromUIApplicationIn the event of distribution, all inherited fromUIControlByUIApplicationDirectly dispatched the incident.

Apple UIControl Documentation

Event delivery priority

test

In order to infer the implementation and delivery mechanism of response events, we do the following tests.

Example 1

IOS event handling, it's enough for me~

hypothesisRootViewSuperViewButtonAll realizetouchesMethod, andButtonAdd tobuttonAction:OfactionClickbuttonThe call after is as follows.

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

Button -> touchesBegan:withEvent:
Button -> touchesEnded:withEvent:
Button -> buttonAction:
Example 2

Or the above view structure, let’s giveRootViewAddUITapGestureRecognizerGestures, and throughtapAction:Method, clickSuperViewAfter that, the method calls as follows.

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

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

IOS event handling, it's enough for me~

In the view aboveSubview1Subview2Subview3Peer view, allSuperViewChild view of. We giveSubview1AddUITapGestureRecognizerGestures, and throughsubView1Action:Method, clickSubview3After that, the method calls as follows.

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

Subview3 -> touchesBegan:withEvent:
Subview3 -> touchesEnded:withEvent:

From the above example, althoughSubview1staySubview3And added gestures. The click area isSubview1andSubview3On both views. But because ofhitTestandpointInsideAfter that, there is no one in the responder chainSubview1SoSubview1The gesture was not responded to.

Analysis

Based on our above tests, we infer the priority of IOS response events and the overall response logic.

When the event comes, it will passhitTestandpointInsideTwo methods, fromWindowStart looking up the view above to find the view of the first responder. After the first responder is found, the system will determine whether it is inherited fromUIControlstillUIResponder, if inherited fromUIControl, directlyUIApplicationSend messages directly to it, and no longer to the responder chain.

If inherited fromUIResponderThe first responder’stouchesBegin, and will not be executed immediatelytouchesEndedInstead, it looks backward along the responder chain after the call. If a gesture is added to a view in the responder chain during the search, enter the proxy method of the gesture. If the proxy method returns to respond to this event, cancel the event of the first responder and call ittouchesCanceledMethod, and then the gesture responds to the event.

If the gesture cannot handle the event, it is left to the first responder. If the first responder is unable to respond to the event, continue to search backward along the responder chain until you find the one that can handle the eventUIResponderObject. If foundUIApplicationIf there is no object responding to the event, the event will be discarded.

Receiving event depth analysis

stayUIApplicationBefore receiving the response event, there is a more complex system level processing, and the processing flow is roughly as follows.

  1. System passedIOKit.frameworkTo handle hardware operations, screen processing is also performed byIOKitCompletion (IOKitMay be the port registered to listen to the screen output)

When the user operates the screen,IOKitAfter receiving the screen operation, the operation will be encapsulated asIOHIDEventObject. adoptmach port(IPC interprocess communication) forward events toSpringBoardTo deal with.

  1. SpringBoardIs the desktop program of IOS system.SpringBoardReceivedmach portIncident, wake upmain runloopTo deal with.

main runloopGive the incident tosource1Handle,source1Would call__IOHIDEventSystemClientQueueCallback()Function.

  1. Inside the function, it will judge whether there is a program displayed in the foreground, and if so, it will pass themach porttakeIOHIDEventThe event is forwarded to this program.

If there is no program in the foreground, it indicatesSpringBoardThe desktop program of is displayed in the foreground, that is, the user operates on the desktop.
__IOHIDEventSystemClientQueueCallback()The function will give the event to thesource0Handle,source0Would call__UIApplicationHandleEventQueue()Function. Specific processing operations will be done inside the function.

  1. For example, if the user clicks the icon of an application, the application will start.

Application receivedSpringBoardThe message will wake upmain runloopAnd give it tosource1Handle,source1call__IOHIDEventSystemClientQueueCallback()Function, the event will be given tosource0Process and callsource0Of__UIApplicationHandleEventQueue()Function.
stay__UIApplicationHandleEventQueue()In the function, theIOHIDEventConvert toUIEventObject.

  1. Inside a function, callUIApplicationOfsendEvent:Method willUIEventPassed to first responder orUIControlObject handling, inUIEventThere are several insideUITouchObject.
Tips

source1yesrunloopUsed for processingmach portSystem events,source0It is used to handle user events.
source1When a system event is received, it is calledsource0So in the end, these events are caused bysource0Processed.

antic

In development, sometimes we find the currentViewAccording to the requirements of the corresponding controller, we can use what we have learned above to find the nearest controller according to the responder chain.

stayUIResponderProvided innextResponderMethod to find the upper level response object of the current response phase. From the currentUIViewStart callingnextResponder, find the object of the upper responder chain, and you can find the nearestUIViewController

Example code:

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