ChatGPT解决这个技术问题 Extra ChatGPT

How can I click a button behind a transparent UIView?

Let's say we have a view controller with one sub view. the subview takes up the center of the screen with 100 px margins on all sides. We then add a bunch of little stuff to click on inside that subview. We are only using the subview to take advantage of the new frame ( x=0, y=0 inside the subview is actually 100,100 in the parent view).

Then, imagine that we have something behind the subview, like a menu. I want the user to be able to select any of the "little stuff" in the subview, but if there is nothing there, I want touches to pass through it (since the background is clear anyway) to the buttons behind it.

How can I do this? It looks like touchesBegan goes through, but buttons don't work.

I thought transparent (alpha 0) UIViews aren’t supposed to respond to touch events?
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.

J
John Stephen

Create a custom view for your container and override the pointInside: message to return false when the point isn't within an eligible child view, like this:

Swift:

class PassThroughView: UIView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        for subview in subviews {
            if !subview.isHidden && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
                return true
            }
        }
        return false
    }
}

Objective C:

@interface PassthroughView : UIView
@end

@implementation PassthroughView
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    for (UIView *view in self.subviews) {
        if (!view.hidden && view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event])
            return YES;
    }
    return NO;
}
@end

Using this view as a container will allow any of its children to receive touches but the view itself will be transparent to events.


Interesting. I need to dive into the responder chain more.
you need to check if the view is visible as well, then you also need to check if the subviews are visible before testing them.
unfortunately this trick does not work for a tabBarController, for which the view cannot be changed. Anybody has an idea to make this view transparent to events too ?
You should also check the alpha of the view. Quite often I'll hide a view by setting the alpha to zero. A view with zero alpha should act like a hidden view.
I did a swift Version: maybe you can include it in the answer for visibility gist.github.com/eyeballz/17945454447c7ae766cb
a
akw

I also use

myView.userInteractionEnabled = NO;

No need to subclass. Works fine.


This will also disable user interaction for any subviews
As @pixelfreak mentioned, this is not the ideal solution for all cases. Cases where the interaction on subviews of that view are still desired require that flag to remain enabled.
j
jackslash

From Apple:

Event forwarding is a technique used by some applications. You forward touch events by invoking the event-handling methods of another responder object. Although this can be an effective technique, you should use it with caution. The classes of the UIKit framework are not designed to receive touches that are not bound to them .... If you want to conditionally forward touches to other responders in your application, all of these responders should be instances of your own subclasses of UIView.

Apples Best Practise:

Do not explicitly send events up the responder chain (via nextResponder); instead, invoke the superclass implementation and let the UIKit handle responder-chain traversal.

instead you can override:

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

in your UIView subclass and return NO if you want that touch to be sent up the responder chain (I.E. to views behind your view with nothing in it).


It would be awesome to have a link to the docs you're quoting, along with the quotes themselves.
I added the links to the relevant places in the documentation for you
~~Could you show how that override would have to look like?~~ Found a answer in another question of how this would look like; it's literally just returning NO;
This should probably be the accepted answer. It's the simplest way and the recommended way.
m
madx

A far simpler way is to "Un-Check" User Interaction Enabled in the interface builder. "If you are using a storyboard"

https://i.stack.imgur.com/HrDA4.png


as others pointed out in a similar answer, this does not help in this case, as it's also disabling touch events in the underlying view. In other words: the button below that view cannot be tapped, regardless if you en- or disable this setting.
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


While this solution may work, it's better to place the relevant parts of your solution within the answer.
T
Tod Cunningham

Building on what John posted, here is an example that will allow touch events to pass through all subviews of a view except for buttons:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    // Allow buttons to receive press events.  All other views will get ignored
    for( id foundView in self.subviews )
    {
        if( [foundView isKindOfClass:[UIButton class]] )
        {
            UIButton *foundButton = foundView;

            if( foundButton.isEnabled  &&  !foundButton.hidden &&  [foundButton pointInside:[self convertPoint:point toView:foundButton] withEvent:event] )
                return YES;
        }        
    }
    return NO;
}

Need to check if the view is visible, and if the subviews are visible.
Perfect solution! An even simpler approach would be to create a UIView subclass PassThroughView that just overrides -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; } if you want to forward any touch events to underneath views.
P
PakitoV

Top voted solution was not fully working for me, I guess it was because I had a TabBarController into the hierarchy (as one of the comments points out) it was in fact passing along touches to some parts of the UI but it was messing with my tableView's ability to intercept touch events, what finally did it was overriding hitTest in the view I want to ignore touches and let the subviews of that view handle them

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *view = [super hitTest:point withEvent:event];
    if (view == self) {
        return nil; //avoid delivering touch events to the container view (self)
    }
    else{
        return view; //the subviews will still receive touch events
    }
}

This should be the answer. Super clean.
B
Barath

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
}

L
LK.

According to the 'iPhone Application Programming Guide':

Turning off delivery of touch events. By default, a view receives touch events, but you can set its userInteractionEnabled property to NO to turn off delivery of events. A view also does not receive events if it’s hidden or if it’s transparent.

http://developer.apple.com/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/EventHandling/EventHandling.html

Updated: Removed example - reread the question...

Do you have any gesture processing on the views that may be processing the taps before the button gets it? Does the button work when you don't have the transparent view over it?

Any code samples of non-working code?


Yes, I wrote in my question that the normal touches work in underneath views, but UIButtons and other elements don't work.
L
Liam

As far as I know, you are supposed to be able to do this by overriding the hitTest: method. I did try it but could not get it to work properly.

In the end I created a series of transparent views around the touchable object so that they did not cover it. Bit of a hack for my issue this worked fine.


n
n8tr

Taking tips from the other answers and reading up on Apple's documentation, I created this simple library for solving your problem:
https://github.com/natrosoft/NATouchThroughView
It makes it easy to draw views in Interface Builder that should pass touches through to an underlying view.

I think method swizzling is overkill and very dangerous to do in production code because you are directly messing with Apple's base implementation and making an application-wide change that could cause unintended consequences.

There is a demo project and hopefully the README does a good job explaining what to do. To address the OP, you would change the clear UIView that contains the buttons to class NATouchThroughView in Interface Builder. Then find the clear UIView that overlays the menu that you want to be tap-able. Change that UIView to class NARootTouchThroughView in Interface Builder. It can even be the root UIView of your view controller if you intend those touches to pass through to the underlying view controller. Check out the demo project to see how it works. It's really quite simple, safe, and non-invasive


C
Community

I created a category to do this.

a little method swizzling and the view is golden.

The header

//UIView+PassthroughParent.h
@interface UIView (PassthroughParent)

- (BOOL) passthroughParent;
- (void) setPassthroughParent:(BOOL) passthroughParent;

@end

The implementation file

#import "UIView+PassthroughParent.h"

@implementation UIView (PassthroughParent)

+ (void)load{
    Swizz([UIView class], @selector(pointInside:withEvent:), @selector(passthroughPointInside:withEvent:));
}

- (BOOL)passthroughParent{
    NSNumber *passthrough = [self propertyValueForKey:@"passthroughParent"];
    if (passthrough) return passthrough.boolValue;
    return NO;
}
- (void)setPassthroughParent:(BOOL)passthroughParent{
    [self setPropertyValue:[NSNumber numberWithBool:passthroughParent] forKey:@"passthroughParent"];
}

- (BOOL)passthroughPointInside:(CGPoint)point withEvent:(UIEvent *)event{
    // Allow buttons to receive press events.  All other views will get ignored
    if (self.passthroughParent){
        if (self.alpha != 0 && !self.isHidden){
            for( id foundView in self.subviews )
            {
                if ([foundView alpha] != 0 && ![foundView isHidden] && [foundView pointInside:[self convertPoint:point toView:foundView] withEvent:event])
                    return YES;
            }
        }
        return NO;
    }
    else {
        return [self passthroughPointInside:point withEvent:event];// Swizzled
    }
}

@end

You will need to add my Swizz.h and Swizz.m

located Here

After that, you just Import the UIView+PassthroughParent.h in your {Project}-Prefix.pch file, and every view will have this ability.

every view will take points, but none of the blank space will.

I also recommend using a clear background.

myView.passthroughParent = YES;
myView.backgroundColor = [UIColor clearColor];

EDIT

I created my own property bag, and that was not included previously.

Header file

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

- (id) propertyValueForKey:(NSString*) key;
- (void) setPropertyValue:(id) value forKey:(NSString*) key;

@end

Implementation File

// NSObject+PropertyBag.m

#import "NSObject+PropertyBag.h"



@implementation NSObject (PropertyBag)

+ (void) load{
    [self loadPropertyBag];
}

+ (void) loadPropertyBag{
    @autoreleasepool {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Swizz([NSObject class], NSSelectorFromString(@"dealloc"), @selector(propertyBagDealloc));
        });
    }
}

__strong NSMutableDictionary *_propertyBagHolder; // Properties for every class will go in this property bag
- (id) propertyValueForKey:(NSString*) key{
    return [[self propertyBag] valueForKey:key];
}
- (void) setPropertyValue:(id) value forKey:(NSString*) key{
    [[self propertyBag] setValue:value forKey:key];
}
- (NSMutableDictionary*) propertyBag{
    if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
    NSMutableDictionary *propBag = [_propertyBagHolder valueForKey:[[NSString alloc] initWithFormat:@"%p",self]];
    if (propBag == nil){
        propBag = [NSMutableDictionary dictionary];
        [self setPropertyBag:propBag];
    }
    return propBag;
}
- (void) setPropertyBag:(NSDictionary*) propertyBag{
    if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
    [_propertyBagHolder setValue:propertyBag forKey:[[NSString alloc] initWithFormat:@"%p",self]];
}

- (void)propertyBagDealloc{
    [self setPropertyBag:nil];
    [self propertyBagDealloc];//Swizzled
}

@end

passthroughPointInside method is working well for me (even without using any of the passthroughparent or swizz stuff - just rename passthroughPointInside to pointInside), thanks a lot.
What is propertyValueForKey?
Hmm. Nobody pointed that out before. I built a custom pointer dictionary that holds properties in a class extension. Ill see if I can find it to include it here.
Swizzling methods is known to be a delicate hacking solution for rare cases and developer fun. It's definitely not App Store safe because it is quite likely to break and crash your app with the next OS update. Why not just override pointInside: withEvent:?
A
Agisight

Try set a backgroundColor of your transparentView as UIColor(white:0.000, alpha:0.020). Then you can get touch events in touchesBegan/touchesMoved methods. Place the code below somewhere your view is inited:

self.alpha = 1
self.backgroundColor = UIColor(white: 0.0, alpha: 0.02)
self.isMultipleTouchEnabled = true
self.isUserInteractionEnabled = true

S
Shaked Sayag

If you can't bother to use a category or subclass UIView, you could also just bring the button forward so that it is in front of the transparent view. This won't always be possible depending on your application, but it worked for me. You can always bring the button back again or hide it.


Hi, welcome to stack overflow. Just a hint for you as a new user: you might want to be careful of your tone. I'd avoid phrases like "if you can't bother"; it might appear as condescending
Thanks for the heads up.. It was more from a lazy standpoint than condescending, but point taken.
t
tBug

Try this

class PassthroughToWindowView: UIView {
        override func test(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            var view = super.hitTest(point, with: event)
            if view != self {
                return view
            }

            while !(view is PassthroughWindow) {
                view = view?.superview
            }
            return view
        }
    } 

W
Wei

I use that instead of override method point(inside: CGPoint, with: UIEvent)

  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.point(inside: point, with: event) else { return nil }
        return self
    }