Views B and C are loaded on view A, B and C overlap, and C overlaps on top of B. At this point, C does not respond when clicking on the overlapping part, and B responds when clicking on the non-overlapping part, the effect of normal response should be achieved. This function is implemented using the hitTest method of UIView.

HitTest principle

HitTest method

func hitTest(_ point: CGPoint.with event: UIEvent?). -> UIView?
Copy the code
  • Point: A point specified in the receiver’s local coordinate system (boundary).
  • Event: The event that the system guarantees to call this method. If this method is called from outside the event handling code, you can specify nil.
  • Return value: Returns all views that can contain point and the last view in view.subviews. If point is completely outside the view hierarchy, nil is returned.

Call to order

The call order of the touch event to find the best responder, i.e. hitTest, is roughly as follows:

touch(UIEvent) - >UIApplication->UIWindow->window.subviews->. ->view
Copy the code
  • When the App receives the touch event, the main thread runloop is woken up, triggering the source1 callback. The source1 callback triggers a Source0 callback that encapsulates the received touch event (IOHIDEvent object) into a UIEvent object, at which point the APP formally begins to respond to the touch event. The source0 callback adds the touch event to the UIApplication’s event queue.

  • UIApplication takes the earliest event from the event queue for distribution. It first passes the event to a window object (UIWindow). If there are more than one UIWindow object, it selects the UIWindow object added last.

  • UIWindow will call its hitTest:withEvent: method to find the most appropriate UIView in the view hierarchy to handle touch events.

The delivery order of touch events

Now that we’ve found the best responder by hitTest, the next thing we need to do is get the best responder to respond to the touch event. The best responder has the power to decide whether to respond to the touch event on its own or to respond to it and pass it on to other responders.

The event transmission sequence is roughly as follows:

view -> superView . - > UIViewController.view -> UIViewController -> UIWindow -> UIApplication-> The event was discardedCopy the code

1. The view first tries to handle the event. If it can’t handle it, the event is passed to its superview

2. The superView also tries to handle the event. If it can’t handle it, it continues to pass its superview uiViewController.view

Uiviewcontroller.view tries to handle the event. If it can’t handle it, it passes the event to UIViewController

UIViewController tries to handle the event. If it can’t handle it, it passes the event to the main Window

The main Window Window attempts to handle the event. If it cannot handle the event, it passes it to the Application singleton

6. If the Application cannot handle it, the event will be discarded.

So when we click on that view, the parent view calls that method in turn, and the click events are the clicked view. The order is that the currently clicked view calls the method and returns the clicked view. The superview then calls that method again, again returning to the click view, and so on.

implementation

Implementation effect

Given that views B and C are loaded on view A respectively, where B is under C, when we click on the overlap between C and B, we ask that the response result returned is the view under the overlap, that is, view B, not default C, and the rest of the click is normal.

Implementation method

When click the view, for his father’s view of child views array, and so traversal, judge whether the child views in the range of the click coordinates, as in this view is not click view, judgment is overlapped view, return to this view, is originally under the click event will be replaced by the view of overlapping view click on view.

The func convert(_ Point: CGPoint, to coordinateSpace: UICoordinateSpace) -> CGPoint method is used to convert the current coordinates to the coordinates of the target view.

Use CALayer’s open Func contains(_ P: CGPoint) -> Bool to determine whether the point falls within the range of the layer of the corresponding view.

Code implementation:

override func hitTest(_ point: CGPoint.with event: UIEvent?). -> UIView? {
    let view = super.hitTest(point, with: event)
    guard view = = self else {
        return view
    }
    guard let superView = view?.superview else {
        return view
    }
    print(String(format: "self:%p, view:%p".self, view!))
    for inView in superView.subviews {
        if let relatePoint = view?.convert(point, to: inView),
inView.layer.contains(relatePoint) && inView ! = view {
            When clicking on the overlap range, change the color of the view below the overlap and return to the view below.
            inView.backgroundColor = fetchColor()
            return inView
        }
    }
    return view
}
Copy the code

Pay attention to the point

1. The point parameter of hitTest is the coordinate corresponding to the object calling it, and we need to traverse and compare the coordinates of the child view of the parent view of the clicked view to determine whether the coordinate falls on the view, so we need to ensure that the called object is the view we clicked, not its parent view. Call this method to check whether the object is itself:

let view = super.hitTest(point, with: event)
guard view = = self else {
    return view
}

Copy the code

2, when using view.layer.contains(point) method to determine coordinates, its coordinates are based on view, and its coordinate range is (0, 0, width, height), need to pay attention to.

Code sample

GitHub:github.com/MichaelLynx…