ChatGPT解决这个技术问题 Extra ChatGPT

Allowing interaction with a UIView under another UIView

Is there a simple way of allowing interaction with a button in a UIView that lies under another UIView - where there are no actual objects from the top UIView on top of the button?

For instance, at the moment I have a UIView (A) with an object at the top and an object at the bottom of the screen and nothing in the middle. This sits on top of another UIView that has buttons in the middle (B). However, I cannot seem to interact with the buttons in the middle of B.

I can see the buttons in B - I've set the background of A to clearColor - but the buttons in B do not seem to receive touches despite the fact that there are no objects from A actually on top of those buttons.

EDIT - I still want to be able to interact with the objects in the top UIView

Surely there is a simple way of doing this?

Its pretty much all explained here: developer.apple.com/iphone/library/documentation/iPhone/… But basically, override hitTest:withEvent:, they even supply a code sample.
I've written a small class just for that. (Added an example in the answers). The solution there a somewhat better than the accepted answer because you can still click a UIButton that is under a semi transparent UIView while the non transparent part of the UIView will still respond to touch events.

g
gyim

You should create a UIView subclass for your top view and override the following method:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // UIView will be "transparent" for touch events if we return NO
    return (point.y < MIDDLE_Y1 || point.y > MIDDLE_Y2);
}

You may also look at the hitTest:event: method.


What is middle_y1/y2 representing here?
Not sure what that return statement is doing, but return CGRectContainsPoint(eachSubview.frame, point) works for me. Extremely helpful answer otherwise
The MIDDLE_Y1/Y2 business is just an example. This function will be "transparent" for touch events in the MIDDLE_Y1<=y<=MIDDLE_Y2 area.
C
Community

While many of the answers here will work, I'm a little surprised to see that the most convenient, generic and foolproof answer hasn't been given here. @Ash came closest, except that there is something strange going on with returning the superview... don't do that.

This answer is taken from an answer I gave to a similar question, here.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView == self) return nil;
    return hitView;
}

[super hitTest:point withEvent:event] will return the deepest view in that view's hierarchy that was touched. If hitView == self (i.e. if there is no subview under the touch point), return nil, specifying that this view should not receive the touch. The way the responder chain works means that the view hierarchy above this point will continue to be traversed until a view is found that will respond to the touch. Don't return the superview, as it is not up to this view whether its superview should accept touches or not!

This solution is:

convenient, because it requires no references to any other views/subviews/objects;

generic, because it applies to any view that acts purely as a container for touchable subviews, and the configuration of the subviews does not affect the way it works (as it does if you override pointInside:withEvent: to return a particular touchable area).

foolproof, there's not much code... and the concept isn't difficult to get your head around.

I use this often enough that I have abstracted it into a subclass to save pointless view subclasses for one override. As a bonus, add a property to make it configurable:

@interface ISView : UIView
@property(nonatomic, assign) BOOL onlyRespondToTouchesInSubviews;
@end

@implementation ISView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView == self && onlyRespondToTouchesInSubviews) return nil;
    return hitView;
}
@end

Then go wild and use this view wherever you might use a plain UIView. Configuring it is as simple as setting onlyRespondToTouchesInSubviews to YES.


The only correct answer. to be clear, if you want to ignore touches to a view, but not to any buttons (say) which are contained in the view, do as Stuart explains. (I usually call it a "holder view" because it can harmlessly "hold" some buttons, but, it does not affect anything "below" the holder.)
Some of the other solutions using points have problems if your view is scrollable, like a UITableView og UICollectionView and you have scrolled up or down. This solution however, works regardless of the scrolling.
T
Tyler

There are several ways you could handle this. My favorite is to override hitTest:withEvent: in a view that is a common superview (maybe indirectly) to the conflicting views (sounds like you call these A and B). For example, something like this (here A and B are UIView pointers, where B is the "hidden" one, that is normally ignored):

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

    if ([B pointInside:pointInB withEvent:event])
        return B;

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

You could also modify the pointInside:withEvent: method as gyim suggested. This lets you achieve essentially the same result by effectively "poking a hole" in A, at least for touches.

Another approach is event forwarding, which means overriding touchesBegan:withEvent: and similar methods (like touchesMoved:withEvent: etc) to send some touches to a different object than where they first go. For example, in A, you could write something like this:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([self shouldForwardTouches:touches]) {
        [B touchesBegan:touches withEvent:event];
    }
    else {
        // Do whatever A does with touches.
    }
}

However, this will not always work the way you expect! The main thing is that built-in controls like UIButton will always ignore forwarded touches. Because of this, the first approach is more reliable.

There's a good blog post explaining all this in more detail, along with a small working xcode project to demo the ideas, available here:

http://bynomial.com/blog/?p=74


The issue with this solution - overriding hitTest on the common superview - is that it doesn't allow the underneath view to work entirely correctly when the underneath view is one of a whole set of scrollable views (MapView etc). Overriding pointInside on the top view as suggested by gyim below works in all cases as far as I can tell.
@delany, That's not true; you can have scrollable views sitting beneath other views and let them both work by overriding hitTest. Here's some sample code: bynomial.com/blogfiles/Temp32.zip
Hey. Not all scrollable views - just some ... I tried your zip code from above, changing the UIScrollView to a (for instance) MKMapView and it doesn't work. Taps work - it's the scrolling that seems to be the issue.
Ok, I checked it out and confirmed that hitTest doesn't work the way you might want with MKMapView. You were right about that, @delany; although it does work correctly with UIScrollView's. I wonder why MKMapView fails?
@Tyler As you mentioned, that "The main thing is that built-in controls like UIButton will always ignore forwarded touches", I wanna know how you know that and whether it is an official document to explain to this behavior. I faced a problem that when UIButton have a plain UIView subview, it will not respond to touch events in the subview's bounds. And I found that the event is forwarded to the UIButton properly as the UIView's default behavior, but I'm not sure it's a designated feature or just a bug. Would you please show me the documentation about this? Thank you so much sincerely.
B
Benjamin Cox

You have to set upperView.userInteractionEnabled = NO;, otherwise the upper view will intercept the touches.

The Interface Builder version of this is a checkbox at the bottom of the View Attributes panel called "User Interaction Enabled". Uncheck it and you should be good to go.


Sorry - should have said. I still want to be able to interact with the objects in the top UIView.
But the upperView can't receive any touch, include the button in the upperView.
This solution work for me. I didn't need the upper view to react to touches at all.
A
Amozoss

Custom implementation of pointInside:withEvent: indeed seemed like the way to go, but dealing with hard-coded coordinates seemed odd to me. So I ended up checking whether the CGPoint was inside the button CGRect using the CGRectContainsPoint() function:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    return (CGRectContainsPoint(disclosureButton.frame, point));
}

S
Segev

Lately I wrote a class that will help me with just that. Using it as a custom class for a UIButton or UIView will pass touch events that were executed on a transparent pixel.

This solution is a somewhat better than the accepted answer because you can still click a UIButton that is under a semi transparent UIView while the non transparent part of the UIView will still respond to touch events.

https://i.stack.imgur.com/Mr3qX.gif

As you can see in the GIF, the Giraffe button is a simple rectangle but touch events on transparent areas are passed on to the yellow UIButton underneath.

Link to class


Since your code isn't that long, you should include the relevant pieces of it in your answer, in case your project is moved or removed at some point in the future.
Thanks, your solution works for my case where I need the non transparent part of my UIView to respond to touch events, while transparent part does not. Brilliant!
@Bruce Glad it helped you!
A
Ash

I guess I'm a bit late to this party, but I'll add this possible solution:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView != self) return hitView;
    return [self superview];
}

If you use this code to override a custom UIView's standard hitTest function, it will ignore ONLY the view itself. Any subviews of that view will return their hits normally, and any hits that would have gone to the view itself are passed up to its superview.

-Ash


this is my preferred method except, i don't think you should be returning [self superview]. the documentation on this method state "Returns the farthest descendant of the receiver in the view hierarchy (including itself) that contains a specified point" and "Returns nil if the point lies completely outside the receiver’s view hierarchy". i think you should be returning nil. when you return nil, control will pass to the superview to for it to check if it has any hits or not. so basically it will do the same thing, except returning superview could break something in the future.
Sure, that'd probably be wise (note the date on my original answer - done a lot more coding since then)
D
Dan Rosenstark

Just riffing on the Accepted Answer and putting this here for my reference. The Accepted Answer works perfectly. You can extend it like this to allow your view's subviews to receive the touch, OR pass it on to any views behind us:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // If one of our subviews wants it, return YES
    for (UIView *subview in self.subviews) {
        CGPoint pointInSubview = [subview convertPoint:point fromView:self];
        if ([subview pointInside:pointInSubview withEvent:event]) {
            return YES;
        }
    }
    // otherwise return NO, as if userInteractionEnabled were NO
    return NO;
}

Note: You don't even have to do recursion on the subview tree, because each pointInside:withEvent: method will handle that for you.


R
Rober

This approach is quite clean and allows that transparent subviews are not reacting to touches as well. Just subclass UIView and add the following method to its implementation:

@implementation PassThroughUIView

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    for (UIView *v in self.subviews) {
        CGPoint localPoint = [v convertPoint:point fromView:self];
        if (v.alpha > 0.01 && ![v isHidden] && v.userInteractionEnabled && [v pointInside:localPoint withEvent:event])
            return YES;
    }
    return NO;
}

@end

Awesome solution!
A
Aditya

Setting userInteraction property disabled might help. Eg:

UIView * topView = [[TOPView alloc] initWithFrame:[self bounds]];
[self addSubview:topView];
[topView setUserInteractionEnabled:NO];

(Note: In the code above, 'self' refers to a view)

This way, you can only display on the topView, but won't get user inputs. All those user touches will through this view and the bottom view will respond for them. I'd use this topView for displaying transparent images, or animate them.


o
outis

My solution here:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint pointInView = [self.toolkitController.toolbar convertPoint:point fromView:self];

    if ([self.toolkitController.toolbar pointInside:pointInView withEvent:event]) {
       self.userInteractionEnabled = YES;
    } else {
       self.userInteractionEnabled = NO;
    }

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

Hope this helps


o
outis

There's something you can do to intercept the touch in both views.

Top view:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
   // Do code in the top view
   [bottomView touchesBegan:touches withEvent:event]; // And pass them on to bottomView
   // You have to implement the code for touchesBegan, touchesEnded, touchesCancelled in top/bottom view.
}

But that's the idea.


This is certainly possible - but is a lot of work (you would have to roll your own touch-sensitive objects in the bottom layer (e.g. buttons), I think?) and it seems odd that one would have to roll your own in this way to get behavior that would seem to be intuitive.
I'm not sure maybe we should just give it a try.
E
Esqarrouth

Here is a Swift version:

override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    return !CGRectContainsPoint(buttonView.frame, point)
}

p
pyRabbit

Swift 3

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    for subview in subviews {
        if subview.frame.contains(point) {
            return true
        }
    }
    return false
}

C
Community

I have never built a complete user interface using the UI toolkit, so I don't have much experience with it. Here is what I think should work though.

Every UIView, and this the UIWindow, has a property subviews, which is an NSArray containing all the subviews.

The first subview you add to a view will receive index 0, and the next index 1 and so forth. You can also replace addSubview: with insertSubview: atIndex: or insertSubview:aboveSubview: and such methods that can determine the position of your subview in the hierarchy.

So check your code to see which view you add first to your UIWindow. That will be 0, the other will be 1. Now, from one of your subviews, to reach another you would do the following:

UIView * theOtherView = [[[self superview] subviews] objectAtIndex: 0];
// or using the properties syntax
UIView * theOtherView = [self.superview.subviews objectAtIndex:0];

Let me know if that works for your case!

(below this marker is my previous answer):

If views need to communicate with each other, they should do so via a controller (that is, using the popular MVC model).

When you create a new view, you can make sure it registers itself with a controller.

So the technique is to make sure your views register with a controller (which can store them by name or whatever you prefer in a Dictionary or Array). Either you can have the controller send a message for you, or you can get a reference to the view and communicate with it directly.

If your view doesn't have a link back the controller (which may be the case) then you can make use of singletons and/or class methods to get a reference to your controller.


Thanks for your reply - but I'm not sure I understand. Both views have a controller - the issue is that, with one view on top of the other, the bottom view is not picking up events (and forwarding them to its controller), even through there is no object actually 'blocking' those events in the top view.
Do you have a UIWindow at the top of our UIViews? If you do, the events should be propagated and you shouldn't need to do any "magic". Have a read about [Window and Views][1] at Apple Dev center (and by all means, add another comment if this doesn't help you!) [1]: developer.apple.com/iphone/library/documentation/iPhone/…
Yes, absolutely - a UIWindow at the top of the hierarchy.
I've updated my answer to include the code for going through a hierarchy of views. Let me know if you need any other help!
Thanks for your help nash - but I'm fairly experienced with building these interfaces and my current one is set up correctly as far as I know. The issue seems to be the default behavior of full views when they are placed on top of others.
P
PapaSmurf

I think the right way is to use the view chain built into the view hierarchy. For your subviews that are pushed onto the main view, do not use the generic UIView, but instead subclass UIView (or one of its variants like UIImageView) to make MYView : UIView (or whatever supertype you want, such as UIImageView). In the implementation for YourView, implement the touchesBegan method. This method will then get invoked when that view is touched. All you need to have in that implementation is an instance method:

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event ;
{   // cannot handle this event. pass off to super
    [self.superview touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event]; }

this touchesBegan is a responder api, so you dont need to declare it in your public or private interface; it's one of those magic api's you just have to know about. This self.superview will bubble up the request eventually to the viewController. In the viewController, then, implement this touchesBegan to handle the touch.

Note that the touches location (CGPoint) is automatically adjusted relative to the encompassing view for you as it is bounced up the view hierarchy chain.


B
Beau Nouvelle

Just want to post this, coz I had somewhat similar problem, spent substantial amount of time trying to implement answers here without any luck. What I ended up doing:

 for(UIGestureRecognizer *recognizer in topView.gestureRecognizers)
 {
     recognizer.delegate=self;
     [bottomView addGestureRecognizer:recognizer];   
 }
 topView.abView.userInteractionEnabled=NO; 

and implementing UIGestureRecognizerDelegate :

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

Bottom view was a navigation controller with number of segues and I had sort of a door on top of it that could close with pan gesture. Whole thing was embedded in yet another VC. Worked like a charm. Hope this helps.


B
BiscottiGelato

Swift 4 Implementation for HitTest based solution

let hitView = super.hitTest(point, with: event)
if hitView == self { return nil }
return hitView

b
brandonscript

Derived from Stuart's excellent, and mostly foolproof answer, and Segev's useful implementation, here is a Swift 4 package that you can drop into any project:

extension UIColor {
    static func colorOfPoint(point:CGPoint, in view: UIView) -> UIColor {

        var pixel: [CUnsignedChar] = [0, 0, 0, 0]

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)

        let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)

        context!.translateBy(x: -point.x, y: -point.y)

        view.layer.render(in: context!)

        let red: CGFloat   = CGFloat(pixel[0]) / 255.0
        let green: CGFloat = CGFloat(pixel[1]) / 255.0
        let blue: CGFloat  = CGFloat(pixel[2]) / 255.0
        let alpha: CGFloat = CGFloat(pixel[3]) / 255.0

        let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)

        return color
    }
}

And then with hitTest:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
    return super.hitTest(point, with: event)
}