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 on A
, the top-most view of the view hierarchy.pointInside:withEvent:
is called recursively on each view.
pointInside:withEvent:
is called on A
, and returns YES
pointInside:withEvent:
is called on B
, and returns NO
pointInside:withEvent:
is called on C
, and returns YES
pointInside:withEvent:
is called on D
, and returns YES
YES
, it will look down on the hierarchy to see the subview where the touch took place. In this case, from A
, C
and D
, it will be D
.D
will be the hit-test viewIt 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:
pointInside:withEvent:
of self
hitTest:withEvent:
returns nil
. the end of the story.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.nil
object in the first time, the first hitTest:withEvent:
returns that object. the end of the story.nil
object, the first hitTest:withEvent:
returns self
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
}
Thanks for answers, they helped me to solve situation with "overlay" views.
+----------------------------+
|A +--------+ |
| |B +------------------+ |
| | |C X | |
| | +------------------+ |
| | | |
| +--------+ |
| |
+----------------------------+
Assume X
- user's touch. pointInside:withEvent:
on B
returns NO
, so hitTest:withEvent:
returns A
. I wrote category on UIView
to handle issue when you need to receive touch on top most visible view.
- (UIView *)overlapHitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1
if (!self.userInteractionEnabled || [self isHidden] || self.alpha == 0)
return nil;
// 2
UIView *hitView = self;
if (![self pointInside:point withEvent:event]) {
if (self.clipsToBounds) return nil;
else hitView = nil;
}
// 3
for (UIView *subview in [self.subviewsreverseObjectEnumerator]) {
CGPoint insideSubview = [self convertPoint:point toView:subview];
UIView *sview = [subview overlapHitTest:insideSubview withEvent:event];
if (sview) return sview;
}
// 4
return hitView;
}
userInteractionEnabled
set to NO
;self
, self
will be considered as potential result.Note, [self.subviewsreverseObjectEnumerator]
needed to follow view hierarchy from top most to bottom. And check for clipsToBounds
to ensure not to test masked subviews.
Usage:
hitTest:withEvent:
with this- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return [self overlapHitTest:point withEvent:event];
}
Official Apple's Guide provides some good illustrations too.
Hope this helps somebody.
It shows like this snippet!
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01)
{
return nil;
}
if (![self pointInside:point withEvent:event])
{
return nil;
}
__block UIView *hitView = self;
[self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
CGPoint thePoint = [self convertPoint:point toView:obj];
UIView *theSubHitView = [obj hitTest:thePoint withEvent:event];
if (theSubHitView != nil)
{
hitView = theSubHitView;
*stop = YES;
}
}];
return hitView;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With