Capturing touches on a preview outside of its scope with hitTest: withEvent:

My problem: I have an EditView supervisor that takes up basically the entire frame of the application, and a subview MenuView that takes up only the bottom ~ 20%, and then the MenuView contains its own subview ButtonView , which is actually outside the boundaries of the MenuView (something like this: ButtonView.frame.origin.y = -100 ).

(Note: EditView has other subqueries that are not part of the hierarchy of the MenuView , but may affect the response.)

You probably already know the problem: when a ButtonView is within a MenuView (or, more specifically, when my touches are within a MenuView ), a ButtonView responds to touch events. When my touches are outside the boundaries of the MenuView (but still within the bounds of the ButtonView ), the ButtonView event ButtonView not receive a touch event.

Example:

  • (E) EditView , the parent of all views
  • (M) MenuView , subtitle EditView
  • (B) - ButtonView , a subview of the MenuView menu

Diagram:

 +------------------------------+ |E | | | | | | | | | |+-----+ | ||B | | |+-----+ | |+----------------------------+| ||M || || || |+----------------------------+| +------------------------------+ 

Since (B) is outside (M), a short press in area (B) 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.

Purpose: I understand that overriding hitTest:withEvent: can solve this problem, but I don't exactly understand it. In my case, if hitTest:withEvent: will be overridden in EditView (my "supervisor")? Or should it be redefined in MenuView , a direct view of a button that does not receive touches? Or am I thinking about it wrong?

If this requires a long explanation, a useful online resource will be useful - with the exception of Apple UIView docs that didn't let me know.

Thank!

+77
ios objective-c
Aug 02 2018-12-12T00:
source share
7 answers

I changed the accepted response code to a more general one - it handles cases where a view makes sub-clicks on its borders, can be hidden and, more importantly: if subviews are complex hierarchies of views, the correct subquery 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.

+125
Feb 14 '13 at 13:13
source share

Ok, I did a bit of work and tested how hitTest:withEvent - at least at a high level. Run this script:

  • (E) - EditView , the parent of all views
  • (M) is MenuView , subtitle EditView
  • (B) - ButtonView , subtitle MenuView

Diagram:

 +------------------------------+ |E | | | | | | | | | |+-----+ | ||B | | |+-----+ | |+----------------------------+| ||M || || || |+----------------------------+| +------------------------------+ 

Since (B) is outside (M), a short press in area (B) 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), tags anywhere in the application will be sent to (M) (or at least he knows about them). You can write code to handle the touch in this case and return the object that should receive the touch.

In particular: the goal of hitTest:withEvent: is to return the object that should receive the hit. So in (M) you can write code as follows:

 // 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 proper ways to write code, please comment.

I hope this helps someone else who addresses this issue later. :)

+27
Aug 03 '12 at 17:39
source share

In Swift 4

 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 } 
+18
Mar 28 '15 at 14:24
source share

What I would do is that ButtonView and MenuView exist at the same level in the hierarchy of views, placing them as in a container whose frame fully matches both of them. Thus, the interactive region of the cropped element will not be ignored due to the boundaries of surveillance.

+2
Aug 02 2018-12-12T00:
source share

If someone needs it, here is a quick alternative

 override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? { if !self.clipsToBounds && !self.hidden && self.alpha > 0 { for subview in self.subviews.reverse() { let subPoint = subview.convertPoint(point, fromView:self); if let result = subview.hitTest(subPoint, withEvent:event) { return result; } } } return nil } 
0
Aug 26 '16 at 17:16
source share

If you have many other objects in the parent view, then most of the other interactive views will probably not work if you use the above solutions, in which 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 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) } } 
0
Nov 03 '17 at 6:51
source share

Put below the lines of code in the hierarchy of your view:

 - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event { UIView* hitView = [super hitTest:point withEvent:event]; if (hitView != nil) { [self.superview bringSubviewToFront:self]; } return hitView; } - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event { CGRect rect = self.bounds; BOOL isInside = CGRectContainsPoint(rect, point); if(!isInside) { for (UIView *view in self.subviews) { isInside = CGRectContainsPoint(view.frame, point); if(isInside) break; } } return isInside; } 

For a more detailed explanation, my blog explained: "goaheadwithiphonetech" in relation to "Custom callout: button is not a problem for clicks."

I hope this helps you ... !!!

-one
May 19 '15 at 10:50
source share



All Articles