Capturing touches on a subview outside the frame of its superview using hitTest:withEvent:
I have modified the accepted answer's code to be more generic - it handles the cases where the view does clip subviews to its bounds, may be hidden, and more importantly : if the subviews are complex view hierarchies, the correct subview will be returned.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.clipsToBounds) {
return nil;
}
if (self.hidden) {
return nil;
}
if (self.alpha == 0) {
return nil;
}
for (UIView *subview in self.subviews.reverseObjectEnumerator) {
CGPoint subPoint = [subview convertPoint:point fromView:self];
UIView *result = [subview hitTest:subPoint withEvent:event];
if (result) {
return result;
}
}
return nil;
}
SWIFT 3
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if clipsToBounds || isHidden || alpha == 0 {
return nil
}
for subview in subviews.reversed() {
let subPoint = subview.convert(point, from: self)
if let result = subview.hitTest(subPoint, with: event) {
return result
}
}
return nil
}
I hope this helps anyone trying to use this solution for more complex use cases.
Ok, I did some digging and testing, here's how hitTest:withEvent
works - at least at a high level. Image this scenario:
- (E) is EditView, the parent of all views
- (M) is MenuView, a subview of EditView
- (B) is ButtonView, a subview of MenuView
Diagram:
+------------------------------+
|E |
| |
| |
| |
| |
|+-----+ |
||B | |
|+-----+ |
|+----------------------------+|
||M ||
|| ||
|+----------------------------+|
+------------------------------+
Because (B) is outside (M)'s frame, a tap in the (B) region will never be sent to (M) - in fact, (M) never analyzes the touch in this case, and the touch is sent to the next object in the hierarchy.
However, if you implement hitTest:withEvent:
in (M), taps anywhere in in the application will be sent to (M) (or it least it knows about them). You can write code to handle the touch in that case and return the object that should receive the touch.
More specifically: the goal of hitTest:withEvent:
is to return the object that should receive the hit. So, in (M) you might write code like this:
// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *subview in self.subviews) {
if (CGRectContainsPoint(subview.frame, point)) {
return subview;
}
}
// use this to pass the 'touch' onward in case no subviews trigger the touch
return [super hitTest:point withEvent:event];
}
I am still very new to this method and this problem, so if there are more efficient or correct ways to write the code, please comment.
I hope that helps anyone else who hits this question later. :)
In Swift 5
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
for member in subviews.reversed() {
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event) else { continue }
return result
}
return nil
}