Event handling for iOS - how hitTest:withEvent: and pointInside:withEvent: are related?
I think you are confusing subclassing with the view hierarchy. What the doc says is as follows. Say you have this view hierarchy. By hierarchy I'm not talking about class hierarchy, but views within views hierarchy, as follows:
+----------------------------+
|A |
|+--------+ +------------+ |
||B | |C | |
|| | |+----------+| |
|+--------+ ||D || |
| |+----------+| |
| +------------+ |
+----------------------------+
Say you put your finger inside D
. Here's what will happen:
hitTest:withEvent:
is called onA
, the top-most view of the view hierarchy.pointInside:withEvent:
is called recursively on each view.pointInside:withEvent:
is called onA
, and returnsYES
pointInside:withEvent:
is called onB
, and returnsNO
pointInside:withEvent:
is called onC
, and returnsYES
pointInside:withEvent:
is called onD
, and returnsYES
- On the views that returned
YES
, it will look down on the hierarchy to see the subview where the touch took place. In this case, fromA
,C
andD
, it will beD
. D
will be the hit-test view
It seems quite a basic question. But I agree with you the document is not as clear as other documents, so here is my answer.
The implementation of hitTest:withEvent:
in UIResponder does the following:
- It calls
pointInside:withEvent:
ofself
- If the return is NO,
hitTest:withEvent:
returnsnil
. the end of the story. - If the return is YES, it sends
hitTest:withEvent:
messages to its subviews. it starts from the top-level subview, and continues to other views until a subview returns a non-nil
object, or all subviews receive the message. - If a subview returns a non-
nil
object in the first time, the firsthitTest:withEvent:
returns that object. the end of the story. - If no subview returns a non-
nil
object, the firsthitTest:withEvent:
returnsself
This process repeats recursively, so normally the leaf view of the view hierarchy is returned eventually.
However, you might override hitTest:withEvent
to do something differently. In many cases, overriding pointInside:withEvent:
is simpler and still provides enough options to tweak event handling in your application.
I find this Hit-Testing in iOS to be very helpful
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
Edit Swift 4:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.point(inside: point, with: event) {
return super.hitTest(point, with: event)
}
guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
return nil
}
for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
if let hitView = subview.hitTest(convertedPoint, with: event) {
return hitView
}
}
return nil
}