How does the system determine which view received the touch?
sobes.tech AI
Answer from AI
The system uses two key mechanisms: hit-testing and view hierarchy.
-
Hit-testing: The UIKit framework uses the
hitTest(_:with:)method of the root view of the window (UIWindow) to determine which view is under the touch point. This method recursively traverses the view hierarchy starting from the root. -
View hierarchy:
- The
hitTest(_:with:)method first checks if the touch point is within the bounds of the current view. If not, it returnsnil. - If the point is inside, it examines the subviews in reverse order (from last added to first). For each subview, it calls its own
hitTest(_:with:)method with the same touch point (converted to the subview's coordinate system). - The first subview whose
hitTest(_:with:)returns a non-nilresult (meaning it or its descendants contain the point) is considered "hit" and this result is returned up the hierarchy. - If no subview contains the point, but the current view contains the point and
userInteractionEnabledistrue, the system considers the touch to have occurred in the current view, and it returnsself. - If
userInteractionEnabledisfalse, orhiddenistrue, oralphais less than 0.01, the method returnsnil, even if the point is inside the bounds.
- The
The process continues down the hierarchy until the lowest view containing the touch point is found. This view becomes the hit-test view or hit-test result, and it (or its closest ancestor) is delegated further event handling (touchesBegan, touchesMoved, etc.).
Here's how you can override hitTest or point(inside:with:) for custom hit-testing behavior:
import UIKit
class CustomView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Check if the point is inside this view.
if !self.point(inside: point, with: event) {
return nil
}
// Traverse subviews in reverse order.
for subview in subviews.reversed() {
// Convert point to subview's coordinate system.
let convertedPoint = subview.convert(point, from: self)
// Recursively call hitTest on subview.
if let hitTestView = subview.hitTest(convertedPoint, with: event) {
// Return the first subview that contains the point.
return hitTestView
}
}
// If no subview contains the point, but this view is interactive and contains the point, return self.
return self
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Default implementation checks if point is within bounds and view is interactive.
// You can customize the hit area here.
let extendedBounds = bounds.insetBy(dx: -20, dy: -20) // Increase touch area by 20pts on all sides.
return extendedBounds.contains(point)
}
}
The point(inside:with:) method is called by hitTest(_:with:) to quickly check if the point is inside the view's bounds before checking subviews.
Ultimately, the lowest interactive view in the hierarchy that contains the touch point and has userInteractionEnabled set to true, hidden set to false, and alpha greater than 0.01 is considered the hit-test view.