Interaction beyond bounds of UIView

43,687

Solution 1

Yes. You can override the hitTest:withEvent: method to return a view for a larger set of points than that view contains. See the UIView Class Reference.

Edit: Example:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(-radius, -radius,
                              self.frame.size.width + radius,
                              self.frame.size.height + radius);

    if (CGRectContainsPoint(frame, point)) {
        return self;
    }
    return nil;
}

Edit 2: (After clarification:) In order to ensure that the button is treated as being within the parent's bounds, you need to override pointInside:withEvent: in the parent to include the button's frame.

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if (CGRectContainsPoint(self.view.bounds, point) ||
        CGRectContainsPoint(button.view.frame, point))
    {
        return YES;
    }
    return NO;
}

Note the code just there for overriding pointInside is not quite correct. As Summon explains below, do this:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
    if ( CGRectContainsPoint(self.oversizeButton.frame, point) )
        return YES;

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

Note that you'd very likely do it with self.oversizeButton as an IBOutlet in this UIView subclass; then you can just drag the "oversize button" in question, to, the special view in question. (Or, if for some reason you were doing this a lot in a project, you'd have a special UIButton subclass, and you could look through your subview list for those classes.) Hope it helps.

Solution 2

@jnic, I am working on iOS SDK 5.0 and in order to get your code working right I had to do this:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(button.frame, point)) {
    return YES;
}
return [super pointInside:point withEvent:event]; }

The container view in my case is a UIButton and all the child elements are also UIButtons that can move outside the bounds of the parent UIButton.

Best

Solution 3

In my case, I had a UICollectionViewCell subclass that contained a UIButton. I disabled clipsToBounds on the cell and the button was visible outside of the cell's bounds. However, the button was not receiving touch events. I was able to detect the touch events on the button by using Jack's answer: https://stackoverflow.com/a/30431157/3344977

Here's a Swift version:

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

    let translatedPoint = button.convertPoint(point, fromView: self)

    if (CGRectContainsPoint(button.bounds, translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, withEvent: event)
    }
    return super.hitTest(point, withEvent: event)
}

Swift 4:

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

    let translatedPoint = button.convert(point, from: self)

    if (button.bounds.contains(translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, with: event)
    }
    return super.hitTest(point, with: event)
}

Solution 4

In the parent view you can override the hit test method:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGPoint translatedPoint = [_myButton convertPoint:point fromView:self];

    if (CGRectContainsPoint(_myButton.bounds, translatedPoint)) {
        return [_myButton hitTest:translatedPoint withEvent:event];
    }
    return [super hitTest:point withEvent:event];

}

In this case, if the point falls within the bounds of your button, you forward the call there; if not, revert to the original implementation.

Solution 5

Why this is happening?

This is because when your subview lies outside of your superview's bounds, touch events that actually happens on that subview will not be delivered to that subview. However, it WILL be delivered to its superview.

Regardless of whether or not subviews are clipped visually, touch events always respect the bounds rectangle of the target view’s superview. In other words, touch events occurring in a part of a view that lies outside of its superview’s bounds rectangle are not delivered to that view. Link

What you need to do?

When your superview receives the touch event mentioned above, you'll need to tell UIKit explicitly that my subview should be the one to receive this touch event.

What about the code?

In your superview, implement func hitTest(_ point: CGPoint, with event: UIEvent?)

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if isHidden || alpha == 0 || clipsToBounds { return super.hitTest(point, with: event) }
        // convert the point into subview's coordinate system
        let subviewPoint = self.convert(point, to: subview)
        // if the converted point lies in subview's bound, tell UIKit that subview should be the one that receives this event
        if !subview.isHidden && subview.bounds.contains(subviewPoint) { return subview }
        return super.hitTest(point, with: event)
    }

Fascinating gotchya: you must go to the "highest too-small superview"

You have to go "up" to the "highest" view which the problem view is outside.

Typical example:

Say you have a screen S, with a container view C. The container viewcontroller's view is V. (Recall V will sit inside of C and be the identical size.) V has a subview (maybe a button) B. B is the problem view which is actually outside of V.

But note that B is also outside of C.

In this example you have to apply the solution override hitTest in fact to C, not to V. If you apply it to V - it does nothing.

Share:
43,687
ThomasM
Author by

ThomasM

Front-end developer (HTML, CSS, Javascript)

Updated on December 30, 2021

Comments

  • ThomasM
    ThomasM over 2 years

    Is it possible for a UIButton (or any other control for that matter) to receive touch events when the UIButton's frame lies outside of it's parent's frame? Cause when I try this, my UIButton doesn't seem to be able to receive any events. How do I work around this?

  • ThomasM
    ThomasM about 13 years
    Can you help me further on implementing this correctly with some sample code? Also I read the reference and because of the following line, I'm confused: "Points that lie outside the receiver’s bounds are never reported as hits, even if they actually lie within one of the receiver’s subviews. Subviews may extend visually beyond the bounds of their parent if the parent view’s clipsToBounds property is set to NO. However, hit testing always ignores points outside of the parent view’s bounds."
  • ThomasM
    ThomasM about 13 years
    It's especially this part that makes it seem as if this is not the solution I need: "However, hit testing always ignores points outside of the parent view’s bounds".. But I could be wrong ofcourse, I find the apple references sometimes really confusing..
  • jnic
    jnic about 13 years
    Ah, I understand now. You need to override the parent's pointInside:withEvent: to include the frame of the button. I will add some sample code to my answer above.
  • RLH
    RLH about 12 years
    Is there a way of doing this without overriding methods of a UIView? For instance, if your views are entirely generic to UIKit and initialized via a Nib, can you override these methods from within your UIViewController?
  • jnic
    jnic about 12 years
    You can't override the methods directly; however, you could override touchesBegan etc. as UIViewController is a UIResponder subclass. Views initialized directly from nibs can still be custom subclasses. Just set the Class field of your view in the Identity Inspector to be your custom subclass and, after loading, cast your nib accordingly.
  • Jeremy Wiebe
    Jeremy Wiebe over 10 years
    One other note about this solution. In hitTest:withEvent: you should return the lowest descendant that matches the hit test. What this means is that if you have a button outside the bounds of the parent view, you would hitTest that button first and return it, then just call 'return [super hitTest:point withEvent:event];'
  • rob5408
    rob5408 almost 10 years
    Shouldn't the frame tested in the first block be CGRectMake(-radius, -radius, self.frame.size.width + 2 * radius, self.frame.size.height + 2 * radius)? Otherwise the overflow on the right and bottom edge is flush with the original right and bottom.
  • Iulian Onofrei
    Iulian Onofrei about 9 years
    The method is not called.
  • Jorge Wander Santana Ureña
    Jorge Wander Santana Ureña almost 8 years
    Thanks, this put me in the right way to go. i override pointInside of my button parent view to return true (on custom frame size) and that made all the work.
  • iosdude
    iosdude almost 7 years
    hitTest:withEvent: and pointInside:withEvent: area not called for me when I press on a button that lies outside of parent's bounds. What can be wrong?
  • ClayJ
    ClayJ about 6 years
    Thank you for this! I have spent hours and hours on an issue stemming from this. I really appreciate your taking the time to post this. :)
  • Hamid Reza Ansari
    Hamid Reza Ansari over 4 years
    That was exactly my scenario . The other point is you should put this method inside the CollectionViewCell Class .
  • Fattie
    Fattie over 4 years
    @ScottZhu : I did add an important gotchya to your answer. Of course, you should feel free to delete or change my addition! Thanks again!
  • rommex
    rommex over 4 years
    But why would you inject the button to an overridden method???