39

I have 3 UIViews, layered on top of one large uiview. I want to know if the user touches the top one and not care about the other ones. I will have a couple of buttons in the second UIView and a UITable in the 3rd UIView.

Problem is I turn userInteractionEngabled on on the first view and that works, but all the other views respond in the same way even if I turn it off. If I disable userInteractionEnabled on self.view none of them respond. I also can't detect which view was touched in the touchesBegan delegate method.

My code:

UIView *aView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 150)];
aView = userInteractionEnabled = YES;
[self.view addSubview:aView];

UIView *bView = [[UIView alloc] initWithFrame:CGRectMake(0, 150, 320, 50)];
bView.userInteractionEnabled = NO;
[self.view addSubview:bView];

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//This gets called for a touch anywhere
}
Michael Currie
  • 13,721
  • 9
  • 42
  • 58
Rudiger
  • 6,749
  • 13
  • 51
  • 102

8 Answers8

65

In order to check whether certain view inside another view was touched you can use hitTest.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

In your custom implementation of touchesBegan check every touch in touches set. The point for hitTest method can be obtained using

- (CGPoint)locationInView:(UIView *)view;

method, where the view is your superView (the one that contains other views).

EDIT: Here's a fast custom implementation:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 
    CGPoint locationPoint = [[touches anyObject] locationInView:self];
    UIView* viewYouWishToObtain = [self hitTest:locationPoint withEvent:event];
}

I hope this was helpful, Paul

Pawel
  • 4,699
  • 1
  • 26
  • 26
  • Sorry, I know you are on the right track but I'm having a little trouble understanding exactly what you are saying. I am getting the point touched from touches from your locationInView and I could technically work out which uiview the user touched from this but I'm not sure how it calls hitTest – Rudiger May 08 '10 at 08:33
  • 2
    It should be noted that calling `locationInView:` with a parameter of `nil` gives you the location `CGPoint` in the coordinate space of the main window. – Dafydd Williams Mar 13 '12 at 00:48
31

In my case the UIView I wanted to find was not returned by the hitTest. But the following code worked better:

    CGPoint locationPoint = [[touches anyObject] locationInView:self.view];
    CGPoint viewPoint = [myImage convertPoint:locationPoint fromView:self.view];
    if ([myImage pointInside:viewPoint withEvent:event]) {
       // do something
    }
Daniel Storm
  • 18,301
  • 9
  • 84
  • 152
rgadbois
  • 441
  • 4
  • 2
  • I also ran into issues with the accepted answer. For whatever reason, this worked as well. Note that I was also trying to detect hits on a UIImageView and not just a UIView if that is relevant at all. – Mic Fok Jun 22 '14 at 23:36
17

swift code:

 override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        if let touch = touches.first {
            if touch.view == self.view {
                self.hideInView()
            } else {
                return
            }

        }
    }
Grzegorz Krukowski
  • 18,081
  • 5
  • 50
  • 71
karthikeyan
  • 3,821
  • 3
  • 22
  • 45
16

This can also be done the following way.

First find the location of the touch in the view you care about:

CGPoint location = [touches.anyObject locationInView:viewYouCareAbout];

Then see if the point is within the bounds of the view:

BOOL withinBounds = CGRectContainsPoint(viewYouCareAbout.bounds, location);

Then if it's within bounds, perform your action.

This can be done with one statement, which for example, can be used directly within an if statement:

CGRectContainsPoint(viewYouCareAbout.bounds, [touches.anyObject locationInView:viewYouCareAbout])
Ben Baron
  • 14,496
  • 12
  • 55
  • 65
9

In touchesBegan check that the touched view is the one you want, solved the problem for me when I tried to identified if the user touched a specific ImageView.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    if ([touch view] ==  imageView ) {
        ... your code to handle the touch
    }
    ...

If you adjust the content for the view diamensions make sure to change the view size accordingly otherwise it will recognize touch event even in area where there in so content in the view. In my case when I tried to keep the image proportion with UIViewContentModeScaleAspectFit although the image was adjusted as required in the "white spaces" areas touch event were catched as well.

Pichirichi
  • 1,440
  • 11
  • 15
8

My swift version to check that touch took place in infoView:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first, infoView.bounds.contains(touch.location(in: infoView))  else { return }
    print("touchesBegan") // Replace by your own code
} // touchesBegan
KerCodex
  • 305
  • 5
  • 9
1

This worked for me:

func respondToGesture(gesture: UIGestureRecognizer) {
    let containsPoint = CGRectContainsPoint(targetView.bounds, gesture.locationInView(targetView))
}
Andrew
  • 3,733
  • 1
  • 35
  • 36
1

When your application receives a touch event, a hit-testing process is initiated to determine which view should receive the event. The process starts with the root of the view hierarchy, typically the application's window, and searches through the subviews in front to back order until it finds the frontmost view under the touch. That view becomes the hit-test view and receives the touch event. Each view involved in this process first tests if the event location is within its bounds. Only after the test is successful, does the view pass the event to the subviews for further hit-testing. So if your view is under the touch but located outside its parent view's bounds, the parent view will fail to test the event location and won't pass the touch event to your view.

One solution to this problem is to modify the layout of the view hierarchy so that your view is inside the bounds of its parent view. If for some reasons the existing layout has to be maintained, you can change the hit-testing behavior of the parent view so that it doesn't ignore the touch events. This can be done by overriding the -(UIView *)hitTest:withEvent: method of the parent view's class

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    // Convert the point to the target view's coordinate system.
    // The target view isn't necessarily the immediate subview
    CGPoint pointForTargetView = [self.targetView convertPoint:point fromView:self];

    if (CGRectContainsPoint(self.targetView.bounds, pointForTargetView)) {

        // The target view may have its view hierarchy,
        // so call its hitTest method to return the right hit-test view
        return [self.targetView hitTest:pointForTargetView withEvent:event];
    }

    return [super hitTest:point withEvent:event]; }

Other possible causes of this problem include:

The userInteractionEnabled property of your view, or any of its parent views, is set to NO. The application called its beginIgnoringInteractionEvents method without a matching call to its endIgnoringInteractionEvents method. You should make sure the userInteractionEnabled property is set to YES and the application does’t ignore the user events if you want the view to be interactive.

If your view doesn’t receive touch events during animation, it is because the animation methods of UIView typically disable touch events while animations are in progress. You can change that behavior by appropriately configuring the UIViewAnimationOptionAllowUserInteraction option when starting the UIView animation.

Another solution is

Add your own custom implementation in the view

class CustomView: YourParentView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        super.hitTest(point, with: event)
        return overlapHitTest(point: point, withEvent: event)
    }
}


extension UIView {

    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

        // 1
        if !self.isUserInteractionEnabled || self.isHidden || self.alpha == 0 {

            return nil
        }

        // 2
        var hitView: UIView? = self
        if !self.point(inside: point, with: event) {

            if self.clipsToBounds {

                return nil
            } else {

                hitView = nil
            }
        }

        // 3
        for subview in self.subviews.reversed() {

            let insideSubview = self.convert(point, to: subview)
            if let sview = subview.overlapHitTest(point: insideSubview, withEvent: event) {
                return sview
            }
        }
        return hitView
    }
}