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

Viewed 36370

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!

8 Answers

If you have many other subviews inside your parent view then probably most of other interactive views would not work if you use above solutions, in that case you can use something like this(In Swift 3.2):

class BoundingSubviewsViewExtension: UIView {

    @IBOutlet var targetView: UIView!

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview
        let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
        if (targetView?.bounds.contains(pointForTargetView!))! {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
        }
        return super.hitTest(point, with: event)
    }
}

I took time to understand how this works exactly because the above solutions didn't work for me as I have many nested subviews. I'll try to explain simply hitTest so everyone can adapt his code depending on the situation.

Imagine this situation : A view called GrandParent has a subview entierly in its bounds called Parent. This Parent has a subview called Child that has bounds outside Parent's bounds :

-------------------------------------- -> Grand parent 
|              ------- -> Child      |
|              |     |               |
|          ----|-----|--- -> Parent  |
|          |   |   ^ |   |           |
|          |   |     |   |           |
|          ----|-----|---            |
|              | +   |               |
|     *        |-----|               |
|                                    |
--------------------------------------

* + and ^ are 3 different user touches. + touch is not recognized on Child Whenever you touch grand parent anywhere in its bound, grand parent will call all its direct subviews .hitTest, no matter where you touched in grand parent. So here, for the 3 touches, grand parent will call parent.hitTest().

hitTest is supposed to return the farthest descendant that contain the given point (the point is given relative to self bounds, in our exemple, Grand Parent call Parent.hitTest() with a point relative to Parent bounds) .

For * touch, parent.hitTest returns nil, and it's perfect. For ^ touch, parent.hitTest returns the Child, because it's in its bounds (default implementation of hitTest).

But for + touch, parent.hitTest returns nil by default, as the touch is not whithin parent's bound. So we need to have our custom implementation of hitTest, in order to basically convert the point relative to Parent's bound to a point relative to Child bounds. Then call hitTest on the child to see if the touch is whithin child bounds (child is supposed to have the default implementation of hitTest, returning the farthest descendant from it whithin its bounds, that is : itself).

If you have complex nested subviews and if the above solution doesn't work very well for you, this explanation may be usefull to make your custom implementation, fitting your view hierarchy.

Related