✍️ Note

Some codes and contents are sourced from Apple’s official documentation. This post is for personal notes where I summarize the original contents to grasp the key concepts

hitTest

Returns the farthest descendant in the view hierarchy of the current view, including itself, that contains the specified point.

Return value

  • The view object that’s the farthest descendent of the current view and contains point. Returns nil if the point lies completely outside the view hierarchy of the current view.

This method traverses the view hierarchy by calling the point(inside:with:) method of each subview to determine which subview to send a touch event to. If point(inside:with:) returns true, this method continues to traverse the subview hierarchy until it finds the frontmost view that contains the specified point. If a view doesn’t contain the point, this method ignores its branch of the view hierarchy. You rarely need to call this method yourself, but you might override it to hide touch events from subviews.

This method ignores view objects that are hidden, that have disabled user interactions, or that have an alpha level less than 0.01. This method doesn’t take the view’s content into account when determining a hit, so it can return a view even if the specified point is in a transparent portion of that view’s content.

This method doesn’t report points that lie outside the view’s bounds as hits, even if they actually lie within one of the view’s subviews. This situation can occur if the view’s clipsToBounds property is false and the affected subview extends beyond the view’s bounds.

Note

When the system calls this method to perform hit-testing for event routing, it expects the resulting view to be part of a UIWindow hierarchy.

https://developer.apple.com/documentation/uikit/uiview/1622469-hittest

Let’s check How it works!

RedView is the farthest view in view hierarchy.

class ViewController: UIViewController {
    @IBOutlet weak var blueView: UIView!
    @IBOutlet weak var greenView: UIView!
    @IBOutlet weak var redView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
        view.addGestureRecognizer(tapGesture)
    }
    @objc func didTap(_ recognizer: UITapGestureRecognizer) {
        let point = recognizer.location(in: view)
        
        let farthestView = view.hitTest(point, with: nil)
        let viewTag = farthestView?.tag
        
        guard let viewTag else {
            return
        }
        switch viewTag {
        case 0:
            print("BlueView")
        case 1:
            print("GreenView")
        case 2:
            print("RedView")
        default:
            break
        }
    }
}

When you tap the center of rectangle, It prints RedView because It’s the farthest view.

point(inside:with:)

Returns a Boolean value indicating whether the receiver contains the specified point. by Apple Document

point

  • A point that is in the receiver’s local coordinate system (bounds).event

The event that warranted a call to this method. If you are calling this method from outside your event-handling code, you may specify nil.

Let’s test.

@objc func didTap(_ recognizer: UITapGestureRecognizer) {
        let point = recognizer.location(in: view)
        
        let isPointInsideBlueView = blueView.point(inside: point, with: nil) //false
        let isPointInsideGreenView = greenView.point(inside: point, with: nil) //false
        let isPointInsideRedView = redView.point(inside: point, with: nil) //false
        
        print(point)
        print(blueView.bounds)
    }

I changed UIView(Blue, Green, and Red) to CustomView to check hitTest and point(inside: CGPoint, with: event)

In fact, when pass the UITapGestureRecognizer’s point which is view based coordinates converted to subview’s coordinates.

See this API

//Point: UITapGestureRecognizer's point based on viewController's view
let point = recognizer.location(in: view)
//convert it to redView's coordinates (bounds)
let convertedPoint = view.convert(point, to: redView)

Summary

hitTest itself calls point(inside:) function to check is bounds contains point or not. It uses Depth First Search.

If point(inside:with:) returns true, this method continues to traverse the subview hierarchy until it finds the frontmost view that contains the specified point. 

https://developer.apple.com/documentation/uikit/uiview/1622469-hittest

Leave a comment

Quote of the week

"People ask me what I do in the winter when there's no baseball. I'll tell you what I do. I stare out the window and wait for spring."

~ Rogers Hornsby