ChatGPT解决这个技术问题 Extra ChatGPT

如何单击透明 UIView 后面的按钮?

假设我们有一个带有一个子视图的视图控制器。子视图占据屏幕的中心,四周有 100 px 的边距。然后,我们在该子视图中添加了一些小东西来点击。我们只是使用子视图来利用新框架(子视图中的 x=0, y=0 实际上是父视图中的 100,100)。

然后,想象我们在子视图后面有一些东西,比如菜单。我希望用户能够在子视图中选择任何“小东西”,但如果那里什么都没有,我希望触摸通过它(因为无论如何背景都是清晰的)到它后面的按钮。

我怎样才能做到这一点?看起来 touchesBegan 通过了,但按钮不起作用。

我认为透明(alpha 0)UIViews 不应该响应触摸事件?
我为此写了一个小班。 (在答案中添加了一个示例)。那里的解决方案比接受的答案要好一些,因为您仍然可以单击半透明 UIView 下的 UIButton,而 UIView 的非透明部分仍将响应触摸事件。

J
John Stephen

为您的容器创建一个自定义视图并覆盖 pointInside: 消息以在该点不在符合条件的子视图中时返回 false,如下所示:

迅速:

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
    }
}

目标 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

将此视图用作容器将允许其任何子级接收触摸,但视图本身将对事件透明。


有趣的。我需要更多地深入响应者链。
您还需要检查视图是否可见,然后您还需要在测试之前检查子视图是否可见。
不幸的是,这个技巧不适用于 tabBarController,因为它的视图不能改变。任何人都想让这个视图对事件透明?
您还应该检查视图的 alpha。我经常通过将 alpha 设置为零来隐藏视图。具有零 alpha 的视图应该像隐藏视图一样工作。
我做了一个快速版本:也许你可以将它包含在可见性的答案中gist.github.com/eyeballz/17945454447c7ae766cb
a
akw

我也用

myView.userInteractionEnabled = NO;

无需子类。工作正常。


这也将禁用任何子视图的用户交互
正如@pixelfreak 提到的,这并不是所有情况下的理想解决方案。仍然需要该视图的子视图上的交互的情况需要该标志保持启用状态。
j
jackslash

From Apple:

事件转发是一些应用程序使用的一种技术。您通过调用另一个响应者对象的事件处理方法来转发触摸事件。尽管这可能是一种有效的技术,但您应该谨慎使用它。 UIKit 框架的类并非旨在接收未绑定到它们的触摸......如果您想有条件地将触摸转发给应用程序中的其他响应者,所有这些响应者都应该是您自己的 UIView 子类的实例。

Apples Best Practise

不要显式地将事件发送到响应者链(通过 nextResponder);相反,调用超类实现并让 UIKit 处理响应者链遍历。

相反,您可以覆盖:

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

在您的 UIView 子类中,如果您希望将触摸发送到响应者链(IE 到您的视图后面的视图,其中没有任何内容),则返回 NO。


拥有指向您引用的文档的链接以及引用本身会很棒。
我为您添加了指向文档中相关位置的链接
~~你能展示一下覆盖的样子吗?~~找到answer in another question关于它的样子;它实际上只是返回 NO;
这应该是公认的答案。这是最简单的方法,也是推荐的方法。
m
madx

一种更简单的方法是在界面构建器中“取消选中”用户交互启用。 “如果你使用故事板”

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


正如其他人在类似的答案中指出的那样,这在这种情况下无济于事,因为它还禁用了底层视图中的触摸事件。换句话说:无论您是启用还是禁用此设置,都无法点击该视图下方的按钮。
S
Segev

最近我写了一个课程来帮助我解决这个问题。将其用作 UIButtonUIView 的自定义类将传递在透明像素上执行的触摸事件。

此解决方案比公认的答案要好一些,因为您仍然可以单击半透明 UIView 下的 UIButton,而 UIView 的非透明部分仍将响应触摸事件。

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

正如您在 GIF 中看到的那样,Giraffe 按钮是一个简单的矩形,但透明区域上的触摸事件会传递到下方的黄色 UIButton

Link to class


虽然此解决方案可能有效,但最好将解决方案的相关部分放在答案中。
T
Tod Cunningham

基于 John 发布的内容,这里有一个示例,它允许触摸事件通过视图的所有子视图(按钮除外):

-(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;
}

需要检查视图是否可见,以及子视图是否可见。
完美的解决方案!更简单的方法是创建一个 UIView 子类 PassThroughView,如果您想将任何触摸事件转发到下面的视图,它只会覆盖 -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }
P
PakitoV

投票最多的解决方案对我来说并不完全有效,我想这是因为我在层次结构中有一个 TabBarController(正如评论之一指出的那样),它实际上是在传递对 UI 某些部分的触摸,但它弄乱了我的tableView 拦截触摸事件的能力,最终它是在视图中覆盖 hitTest 我想忽略触摸并让该视图的子视图处理它们

- (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
    }
}

这应该是答案。超级干净。
B
Barath

斯威夫特 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.

根据“iPhone应用程序编程指南”:

关闭触摸事件的传递。默认情况下,视图接收触摸事件,但您可以将其 userInteractionEnabled 属性设置为 NO 以关闭事件传递。如果视图是隐藏的或透明的,它也不会接收事件。

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

更新:删除示例 - 重新阅读问题...

在按钮获取之前,您是否对可能正在处理点击的视图进行了任何手势处理?当您没有透明视图时,该按钮是否有效?

任何非工作代码的代码示例?


是的,我在我的问题中写道,正常的触摸在视图下方起作用,但 UIButtons 和其他元素不起作用。
L
Liam

据我所知,您应该能够通过覆盖 hitTest: 方法来做到这一点。我确实尝试过,但无法使其正常工作。

最后,我在可触摸对象周围创建了一系列透明视图,这样它们就不会覆盖它。我的问题有点破解,这很好用。


n
n8tr

从其他答案中获取提示并阅读 Apple 的文档,我创建了这个简单的库来解决您的问题:
https://github.com/natrosoft/NATouchThroughView
它可以轻松地在 Interface Builder 中绘制视图,这些视图应该将触摸传递到底层看法。

我认为方法调配在生产代码中是矫枉过正且非常危险的,因为您直接弄乱了 Apple 的基本实现并进行了可能导致意外后果的应用程序范围的更改。

有一个演示项目,希望自述文件能很好地解释要做什么。要解决 OP,您需要将包含按钮的清晰 UIView 更改为 Interface Builder 中的 NATouchThroughView 类。然后找到覆盖您想要点击的菜单的清晰 UIView。在 Interface Builder 中将该 UIView 更改为 NARootTouchThroughView 类。如果您打算将这些触摸传递到底层视图控制器,它甚至可以是您的视图控制器的根 UIView。查看演示项目以了解它是如何工作的。它真的非常简单、安全、无创


C
Community

我创建了一个类别来做到这一点。

一点方法调和,视图是金色的。

标题

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

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

@end

实施文件

#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

您需要添加我的 Swizz.h 和 Swizz.m

位于Here

之后,您只需在 {Project}-Prefix.pch 文件中导入 UIView+PassthroughParent.h,每个视图都将具有此功能。

每个视图都会拿分,但没有一个空格会。

我还建议使用清晰的背景。

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

编辑

我创建了自己的财产包,以前没有包括在内。

头文件

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

实施文件

// 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 方法对我来说效果很好(即使不使用任何 passthroughparent 或 swizz 的东西——只需将 passthroughPointInside 重命名为 pointInside),非常感谢。
什么是 propertyValueForKey?
唔。之前没有人指出这一点。我构建了一个自定义指针字典,其中包含类扩展中的属性。生病看看我能不能找到它把它包括在这里。
众所周知,Swizzling 方法是一种微妙的黑客解决方案,适用于罕见的情况和开发人员的乐趣。这绝对不是 App Store 安全的,因为它很可能会在下一次操作系统更新时破坏和崩溃您的应用程序。为什么不直接覆盖 pointInside: withEvent:
A
Agisight

尝试将 transparentViewbackgroundColor 设置为 UIColor(white:0.000, alpha:0.020)。然后您可以在 touchesBegan/touchesMoved 方法中获取触摸事件。将下面的代码放在你的视图被初始化的地方:

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

S
Shaked Sayag

如果您懒得使用类别或子类 UIView,您也可以将按钮向前移动,使其位于透明视图的前面。根据您的应用程序,这并不总是可能的,但它对我有用。您可以随时将按钮重新带回或隐藏它。


嗨,欢迎堆栈溢出。对您作为新用户的提示:您可能需要注意自己的语气。我会避免使用诸如“如果您不能打扰”之类的短语;它可能显得居高临下
感谢您的提醒。从懒惰的角度来看,这比居高临下,但重点是。
t
tBug

尝试这个

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

我使用它而不是覆盖方法点(内部:CGPoint,带有:UIEvent)

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