UIView. Why Does A Subviews Outside its Parent's Extent Not Receive Touches?
Asked Answered
G

5

38

I have a simple - trivial - UIView parent/child hierarchy. One parent (UIView). One child (UIButton). The parents bounds are smaller then it's child's bounds so that a portion of the child extends beyond the bounding box of its parent.

Here's the problem: Those portions of the child outside the bbox of the parent do not receive touches. Only tapping within the bbox of the parent allows the child button to receive touches.

Can someone please suggest a fix/workaround?

UPDATE

For those following this question, here is the solution I implemented as a result of @Bastians most excellent answer:

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

    BOOL isInside = [super pointInside:point withEvent:event];

    // identify the button view subclass
    UIButton *b = (UIButton *)[self viewWithTag:3232];
    CGPoint inButtonSpace = [self convertPoint:point toView:b];

    BOOL isInsideButton = [b pointInside:inButtonSpace withEvent:nil];

    if (isInsideButton) {

        return isInsideButton;

    } // if (YES == isInsideButton)

    return isInside;        
}
Gilson answered 30/4, 2011 at 20:14 Comment(3)
Just wanted to point out that your hitTest:withEvent: override is not actually changing anything since it's just calling the super's implementation. So it's not necessary.Apotheosis
@dugla: Please remove your solution from the question and post it as an answer instead.Howey
In which subclass do you override pointInside:, the child view?Related
B
27

The problem is the responder chain. When you touch the display it will go down from the parents to the childen.

So .. when you touch the screen the parent will see that the touch is outside of it's own bounds and so the children will not even asked.

The function that does that is the hitTest. If you have your own UIView class you can overwrite it and return the button by yourself.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
Bidget answered 30/4, 2011 at 20:22 Comment(4)
I just re-read the UIView docs and saw this. Ok, so to sketch this out, within the parent's hitTest:withEvent: would I simply call the [self pointInside:withEvent] and then explicity call the child's hitTest:withEvent when the point lies with in the child's bbox but outside the parent's bbox?Gilson
Yes ... that's a possibility .. or you can directly return the button object in the parents hittestBidget
still not quite grokking the intent of hitTest:withEvent as distinct from pointInside:withEvent:. Putting aside my example for the moment, does hitTest:withEvent: return nil for touches outside the bbox of a views bounds? If so then I still need bbox testing logic to identify hits within the portion of the child outside it's parents bbox. Yes?Gilson
I went with an overridden solution for both relavent methods.Gilson
A
5

Per Apple’s own documentation, the simplest and most reliable way I have found to do this is to override hitTest:withEvent: in the superclass of your clipped view to look like the following:

- (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];
}
Attractive answered 30/8, 2014 at 18:37 Comment(0)
F
3
  • @dugla, thank you for the question!
  • @Bastian and @laoyur, thanks to you for answers!

Swift 4

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {

    if yourChildView.point(inside: convert(point, to: yourChildView), with: event) {
        return true
    }

    return super.point(inside: point, with: event)
}
Fumble answered 14/2, 2018 at 22:5 Comment(0)
D
2

Precondition:

You have a UIButton(named as button1) inside a UIView(named as container), and button1 is partially outside the container's bounds.

Problem:

the part outside the container of button1 will not response click.

Solution:

subclass your container from UIView:

class Container: UIView {
    override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {

        let closeButton = viewWithTag(10086) as! UIButton  //<-- note the tag value
        if closeButton.pointInside(convertPoint(point, toView: closeButton), withEvent: event) {
            return true
        }
        return super.pointInside(point, withEvent: event)
    }
}

Don't forget to give your button1 a tag of 10086

Deciare answered 21/12, 2016 at 6:23 Comment(0)
A
-1

I had the same exact problem at hand. You only need to override:

-(BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event

Here is working code for a custom UIView subclass created solely to be tappable for its out-of-bounds children.

@implementation ARQViewToRightmost

// We should not need to override this one !!!
/*-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self isInside:point])
        return self;
    return nil;
}*/

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self isInside:point])
        return YES; // That answer will make hitTest continue recursively probing the view's subviews for hit
    return NO;
}

// Check to see if the point is within the bounds of the view. We've extended the bounds to the max right
// Make sure you do not have any other sibling UIViews to the right of this one as they will never receive events
- (BOOL) isInside:(CGPoint)point
{
    CGFloat maxWidth = CGFLOAT_MAX;
    CGRect rect = CGRectMake(0, 0, maxWidth, self.frame.size.height);
    if (CGRectContainsPoint(rect, point))
        return YES;
    return NO;
}

@end
Afford answered 4/6, 2013 at 10:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.