toblerpwn toblerpwn - 3 months ago 42
Objective-C Question

Capturing touches on a subview outside the frame of its superview using hitTest:withEvent:

My problem: I have a superview

EditView
that takes up basically the entire application frame, and a subview
MenuView
which takes up only the bottom ~20%, and then
MenuView
contains its own subview
ButtonView
which actually resides outside of
MenuView
's bounds (something like this:
ButtonView.frame.origin.y = -100
).

(note:
EditView
has other subviews that are not part of
MenuView
's view hierarchy, but may affect the answer.)

You probably already know the issue: when
ButtonView
is within the bounds of
MenuView
(or, more specifically, when my touches are within
MenuView
's bounds),
ButtonView
responds to touch events. When my touches are outside of
MenuView
's bounds (but still within
ButtonView
's bounds), no touch event is received by
ButtonView
.

Example:


  • (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.

Goal: I gather that overriding
hitTest:withEvent:
can solve this problem, but I don't understand exactly how. In my case, should
hitTest:withEvent:
be overridden in
EditView
(my 'master' superview)? Or should it be overridden in
MenuView
, the direct superview of the button that is not receiving touches? Or am I thinking about this incorrectly?

If this requires a lengthy explanation, a good online resource would be helpful - except Apple's UIView docs, which have not made it clear to me.

Thanks!

Answer

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 && !self.hidden && self.alpha > 0) {
        for (UIView *subview in self.subviews.reverseObjectEnumerator) {
            CGPoint subPoint = [subview convertPoint:point fromView:self];
            UIView *result = [subview hitTest:subPoint withEvent:event];
            if (result != nil) {
                return result;
            }
        }
    }

    return nil;
}

I hope this helps anyone trying to use this solution for more complex use cases.