72

I have a UIView with 4 buttons on it and another UIView on top of the buttons view. The top most view contains a UIImageView with a UITapGestureRecognizer on it.

The behavoir I am trying to create is that when the user taps the UIImageView it toggles between being small in the bottom right hand corner of the screen and animating to become larger. When it is large I want the buttons on the bottom view to be disabled and when it is small and in the bottom right hand corner I want the touches to be passed through to the buttons and for them to work as normal. I am almost there but I cannot get the touches to pass through to the buttons unless I disable the UserInteractions of the top view.

I have this in my initWithFrame: of the top view:

// Add a gesture recognizer to the image view
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageTapped:)];
tapGestureRecognizer.cancelsTouchesInView = NO;
[imageView addGestureRecognizer:tapGestureRecognizer];
[tapGestureRecognizer release];

and I this is my imageTapped: method:

- (void) imageTapped:(UITapGestureRecognizer *) gestureRecognizer {
    // Toggle between expanding and contracting the image
    if (expanded) {
        [self contractAnimated:YES];
        expanded = NO;
        gestureRecognizer.cancelsTouchesInView = NO;
        self.userInteractionEnabled = NO;
        self.exclusiveTouch = NO;
    }
    else {
        [self expandAnimated:YES];
        expanded = YES;
        gestureRecognizer.cancelsTouchesInView = NO;
        self.userInteractionEnabled = YES;
        self.exclusiveTouch = YES;
    }
}

With the above code, when the image is large the buttons are inactive, when I touch the image it shrinks and the buttons become active. However, the small image doesn't receive the touches and therefore wont expand.

If I set self.userInteractionEnabled = YES in both cases, then the image expands and contracts when touched but the buttons never receive touches and act as though disabled.

Is there away to get the image to expand and contract when touched but for the buttons underneath to only receive touches if the image is in its contracted state? Am I doing something stupid here and missing something obvious?

I am going absolutely mad trying to get this to work so any help would be appreciated,

Dave

UPDATE:

For further testing I overrode the touchesBegan: and touchesCancelled: methods and called their super implementations on my view containing the UIImageView. With the code above, the touchesCancelled: is never called and the touchesBegan: is always called.

So it would appear that the view is getting the touches, they are just not passed to the view underneath.

UPDATE

Is this because of the way the responder chain works? My view hierarchy looks like this:

VC - View1
     -View2
      -imageView1 (has tapGestureRecogniser)
      -imageView2
     -View3
      -button1
      -button2

I think the OS first does a hitTest as says View2 is in front so should get all the touches and these are never passed on to View3 unless userInteractions is set to NO for View2, in which case the imageView1 is also prevented from receiving touches. Is this how it works and is there a way for View2 to pass through it's touches to View3?

KlimczakM
  • 11,178
  • 10
  • 55
  • 76
Magic Bullet Dave
  • 8,806
  • 9
  • 48
  • 81
  • How are you expanding and contracting your imageView? edit: I'm assuming you're still covering up the buttons somehow with your imageView even though it's visually contracted. – Sean Berry Jan 26 '12 at 22:15
  • The UIImageView is a subview of another view. It is this view that is covering the buttons and when you see self in my code it is a subclass of UIView with the UIImageView added as a subclass. Hope that makes sense. – Magic Bullet Dave Jan 26 '12 at 23:16
  • See http://khanlou.com/2018/09/hacking-hit-tests for a good discussion of this. – Andrew Duncan Apr 02 '19 at 19:14

5 Answers5

94

The UIGestureRecognizer is a red herring I think. In the end to solve this I overrode the pointInside:withEvent: method of my UIView:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    BOOL pointInside = NO;

    if (CGRectContainsPoint(imageView.frame, point) || expanded) pointInside = YES;

    return pointInside;
}

This causes the view to trap all touches if you touch either the imageView or if its expanded flag is set. If it is not expanded then only trap the touches if they are on the imageView.

By returning NO, the top level VC's View queries the rest of its view hierarchy looking for a hit.

Magic Bullet Dave
  • 8,806
  • 9
  • 48
  • 81
  • 24
    Thanks for the info in this answer! However, I ended up overriding `hitTest` rather than `pointInside`. This is because I was trying to make sure the *parent* view did not steal any taps, but wanted all the child views to be able to. By overriding `hitTest` you can simply call the base implementation to determine which view was tapped, and if that view happens to be `self` then return `nil`. – Kirk Woll Aug 19 '13 at 17:33
  • 1
    below solution is better. uncheck "User interaction enabled" directly from the Storyboard – coolcool1994 Nov 27 '16 at 21:32
66

Select your View in Storyboard or XIB and...


enter image description here


Or in Swift

view.isUserInteractionEnabled = false
Michael
  • 8,464
  • 2
  • 59
  • 62
6

Look into the UIGestureRecognizerDelegate Protocol. Specifically, gestureRecognizer:shouldReceiveTouch:

You'll want to make each UIGestureRecognizer a property of your UIViewController,

// .h
@property (nonatomic, strong) UITapGestureRecognizer *lowerTap;

// .m
@synthesize lowerTap;

// When you are adding the gesture recognizer to the image view
self.lowerTap = tapGestureRecognizer

Make sure you make your UIViewController a delegate,

[self.lowerTap setDelegate: self];

Then, you'd have something like this,

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    if (expanded && gestureRecognizer == self.lowerTap) {    
        return NO;
    }
    else {
        return YES;
    }
}

Of course, this isn't exact code. But this is the general pattern you'd want to follow.

john
  • 2,933
  • 4
  • 25
  • 45
  • 2
    HI awfullyjohn, had a look at the delegate protocol and I can't see how this would work for me. I want the gesture recognizer to always receive the touches, I just sometimes want to be able to pass them on to other views in the window rather than cancelling them. According to the docs cancelsTouchesInView = NO should pass them through, which they do as long as the parent view has userInteractionEnabled set to NO also. – Magic Bullet Dave Jan 26 '12 at 23:27
  • What about the touch do you want to pass on? For example, instead of simply returning `YES` in the `else` clause, you could call the method that would be called anyhow by the other views. – john Jan 26 '12 at 23:34
  • Ah, I see where you are going with that. I can't really do that I'm afraid. Think of my view subclass as a control. I knows about its state and can change it based on user input but it knows nothing about what it has been placed onto, or any methods that would be invoked. Does that make sense? – Magic Bullet Dave Jan 26 '12 at 23:53
  • This is generally speaking the 100% correct answer. Just use shouldReceiveTouch to pass through clicks. Good one, John – Fattie Aug 03 '14 at 19:10
  • @awfullyjohn, What if you need the topmost `UIView` to pass the events? You can't make a subclass. – Iulian Onofrei Apr 11 '16 at 14:47
3

I have a another solution. I have two views, let's call them CustomSubView that were overlapping and they should both receive the touches. So I have a view controller and a custom UIView class, lets call it ViewControllerView that I set in interface builder, then I added the two views that should receive the touches to that view.

So I intercepted the touches in ViewControllerView by overwriting hitTest:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    return self;
}

Then I overwrote in ViewControllerView:

- (void)touchesBegan:(NSSet *)touches
           withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    for (UIView *subview in [self.subviews reverseObjectEnumerator])
    {
        if ([subview isKindOfClass:[CustomSubView class]])
        {
            [subview touchesBegan:touches withEvent:event];
        }
    }
}

Do the exact same with touchesMoved touchesEnded and touchesCancelled.

Nico S.
  • 2,023
  • 1
  • 22
  • 49
3

@Magic Bullet Dave's solution but in Swift

Swift 3

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    var pointInside = false
    if commentTextField.frame.contains(point) {
        pointInside = true
    } else {
        commentTextField.resignFirstResponder()
    }
    return pointInside
}

I use it in my CameraOverlayView for ImagePickerViewController.cameraOverlay to give user ability to comment while taking new photo

Community
  • 1
  • 1
imike
  • 4,910
  • 2
  • 34
  • 39