I've created a UIView (tooltip box) and added it as a subview of another view.
Using anchor constraints, I've floated it off the edge of its superview where it hovers above an unrelated UIView (which happens to be UITextView)
To the trespassing tooltip view I've added a UIButton and a UISegmentedControl
When I tap either button or segmented control, the text view, which does not own the tooltip view, gets the event, not the button or segmented control.
My question is, how can I make UIButton or UISegmentedControl get their event and not the text view?
UPDATE 1: (a theory)
I think what's going on has something to do with the fact that the UIView is a UIResponder, however the UITextView is currently the first responder. The view controller creating tooltip view hosts a keyboard accessory view, wherein the keyboard (and thus the accessory view controller and its accessory view) appeared because the text view became first responder.
Do I need to make the tooltip view first responder? That would be a mess, not the least of which because the accessorized keyboard, to which the tooltip is a subview, would go away. Further, the text view constraints change when it stops being first responder, so that it slides down and other views shift in when text view loses first responder status.
UPDATE 2: (Apple says I'm a bad boy, turns out, I am, and I can prove it)
Checking Apple docs related to reordering responder chain, I found the following explicit note (consider it a warning):
Note:
If a touch location is outside of a view’s bounds, the hitTest(:with:) method ignores that view and all of its subviews. As a result, when a view’s clipsToBounds property is false, subviews outside of that view’s bounds are not returned even if they happen to contain the touch. For more information about the hit-testing behavior, see the discussion of the hitTest(_:with:) method in UIView.
So I tested that:
I created a subclass of UIView and overrode the hitTest() method with print(#function).
I observed two things:
When the tool tip UIView is off the edge of the keyboard accessory UIView:
A. Buttons aren't responded to
B. The tool tip view's overridden hittest() isn't called!
When I change constraints to position tooltip on top of the keyboard accessory view, for the buttons that do overlap the accessory view (e.g. first-responder/superview) bounds, hittest() is called, and the buttons work as expected. Buttons in the part of the tooltip view still hanging over the edge of the keyboard accessory don't feel the touch, nor is hittest() called).. the underlying text view gets them.
Conclusion: Apple's explicit note describes my problem.
UPDATE 3 (Will gesture recognizers work?)
I don't want my interactive tooltip above the keyboard accessory.
Based on the fact that gesture recognizers are handled before UIView event handling is invoked, I wonder if the solution is to add (otherwise redundant) tap gesture recognizers to the UIControls (button & seg control), knowing their parent view is out of bounds of the responder chain?
So my new question is: How can I make a UIView that's way over the edge of a non-clipped receiver UIView have its buttons work?
We can implement
hitTest()to allow touches outside the bounds of the view to be used.But, we need to check for the view that was touched.
So, you said you have a subview (I'll call it
tipView) that contains a button and a segmented control as subviews, and you're adding thattipViewas a subview of aUITextViewpositioned outside the bounds of the text view.An example could be:
and it will look like this at run-time:
Assuming that is close to what you're going for...
As you've seen, you cannot tap the button or the segmented control.
So, we need to add the
hitTest()to our customToolTipTextViewclass:Because the view, button and seg control are not inside the view's bounds, tapping the button will result in a touch-point relative to the text view -- tapping the button may give you this point:
(167.0, -59.5).So, we convert that point to the
tipViewcoordinate space, and see if it is contained in either the button or seg control's frame. If so, we return that view.If not, we call
superto let UIKit handle the touch... either activating the text view if the touch is inside, or allowing the touch to reach some other UI element on the screen.