允许在另一个 UIView 下与 UIView 交互

有没有一种简单的方法允许与位于另一个 UIView 下的 UIView 中的一个按钮进行交互——在这个按钮的顶部 UIView 中没有实际的对象?

例如,现在我有一个 UIView (A) ,顶部有一个对象,屏幕底部有一个对象,中间什么都没有。它位于另一个中间有按钮的 UIView (B)之上。然而,我似乎不能与 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 -我仍然希望能够与顶部 UIView 中的对象进行交互

肯定有一个简单的方法可以做到这一点吧?

67884 次浏览

您必须设置 upperView.userInteractionEnabled = NO;,否则上视图将拦截触摸。

这个 Interface Builder 的版本是在视图属性面板底部的一个复选框,名为“启用用户交互”。取消检查,你应该可以走了。

我从来没有使用 UI 工具包构建过完整的用户界面,所以我没有太多使用它的经验。不过我认为应该这样做。

每个 UIView,这个 UIWindow,都有一个属性 subviews,它是一个包含所有子视图的 NSArray。

添加到视图中的第一个子视图将接收索引0,下一个索引1,以此类推。您还可以用 insertSubview: atIndex:insertSubview:aboveSubview:替换 addSubview:,这些方法可以确定子视图在层次结构中的位置。

因此,请检查代码以查看首先向 UIWindow 添加哪个视图。那是0,另一个是1。
现在,从你的一个子视图,到达另一个子视图,你会做以下事情:

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!


(下面是我之前的答案) :

如果视图需要彼此通信,它们应该通过控制器(即使用流行的 车祸模型)进行通信。

创建新视图时,可以确保它向控制器注册自己。

因此,技术是确保您的视图注册了一个控制器(它可以按照名称或任何您喜欢的方式在 Dictionary 或 Array 中存储它们)。您可以让控制器为您发送消息,或者您可以获得对视图的引用并与其直接通信。

如果您的视图没有与控制器的链接(可能是这种情况) ,那么您可以使用 单身和/或类方法来获得对控制器的引用。

您可以做一些事情来拦截两个视图中的接触。

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.

您应该为您的顶视图创建一个 UIView 子类并覆盖以下方法:

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

您还可以查看 hitTest: event: method。

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.

另一种方法是事件转发,这意味着覆盖 touchesBegan:withEvent:和类似的方法(如 touchesMoved:withEvent:等) ,以发送一些触摸到一个不同的对象,而不是他们第一次去。例如,在 A 中,你可以这样写:

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

然而,这不会永远奏效的的方式你期望!主要的问题是,像 UIButton 这样的内置控件总是会忽略转发的触摸。因此,第一种方法更可靠。

有一篇很好的博客文章详细解释了这一切,同时还有一个小型的 xcode 项目来演示这些想法,可以在这里找到:

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

我的解决办法是:

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

希望这个能帮上忙

PointInside: withEvent: 的自定义实现看起来确实像是要走的路,但是处理硬编码的坐标对我来说似乎有些奇怪。因此,我最终使用 CGRectContainsPoint ()函数检查了 CGPoint 是否在 CGRect 按钮的内部:

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

我想我来得有点晚了,不过我要补充一个可能的解决办法:

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

我认为正确的方法是使用内置在视图层次结构中的视图链。 对于推送到主视图的子视图,不要使用通用的 UIView,而是使用子类 UIView (或其变体之一,如 UIImageView)来创建 MYView: UIView (或任何你想要的超类型,如 UIImageView)。在 YourView 的实现中,实现 touch esBegan 方法。然后,当触及该视图时,将调用此方法。在这个实现中,您所需要的只是一个实例方法:

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

这些触摸 Began 是一个响应 API,所以你不需要在你的公共或私有界面中声明它; 这是一个你需要知道的神奇的 api。这个 self. superview 将最终向 viewController 显示请求。然后,在 viewController 中实现这个触摸。

请注意,当触点位置(CGPoint)在视图层次结构链上反弹时,它会自动相对于包围视图进行调整。

设置 userInteractive 属性禁用可能会有帮助。例如:

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

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

这样,您只能在 topView 上显示,但不能获得用户输入。所有这些用户触摸将通过这个视图和底部视图将响应他们。我会使用这个 topView 来显示透明图像,或者给它们赋予动画效果。

我只是随便写一下“接受的答案”,然后把这个放在这里供我参考。接受的答案是完美的。你可以像这样扩展它,让你的视图的子视图接收触摸,或者把它传递给我们身后的任何视图:

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

注意: 您甚至不需要在子视图树上执行递归操作,因为每个 pointInside:withEvent:方法都会为您处理这个问题。

这种方法是相当干净的,并允许 透明的副检视不反应触摸以及。只需子类 UIView并在其实现中添加以下方法:

@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

只是想发布这个,因为我有一些类似的问题,花了大量的时间试图实现这里的答案没有任何运气。我最后做了什么:

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

底部的视图是一个导航控制器,有很多转折点,我在它上面有一扇门,可以通过平移手势关闭。整个事情都嵌入到另一个风险投资中。非常有效。希望这个能帮上忙。

虽然这里的许多答案都会起作用,但我有点惊讶地发现,这里没有给出最方便、最通用和最简单的答案。@ Ash 离得最近,除了归还监控录像有点奇怪... 别这样。

这个答案来自于我对一个类似问题 给你的回答。

- (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]将返回该视图层次结构中被触及的最深视图。如果 hitView == self(也就是说,如果接触点下面没有子视图) ,返回 nil,指定这个视图应该 没有接收触摸。响应链的工作方式意味着视图层次结构将继续遍历这个点,直到找到一个视图将响应触摸。不要返回超视图,因为它不是由这个视图,它的超视图应该接受触摸或不!

这个解决方案是:

  • 方便 ,因为它不需要引用任何其他视图/子视图/对象;
  • 通用 ,因为它适用于任何纯粹作为可触摸子视图容器的视图,子视图的配置不影响它的工作方式(如果你覆盖 pointInside:withEvent:返回一个特定的可触摸区域,它就会这样做)。
  • 万无一失的 ,没有太多的代码... ... 而且这个概念并不难理解。

我经常使用它,所以我将它抽象为一个子类,以便将无意义的视图子类保存为一个重写。作为额外收获,添加一个属性使其可配置:

@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

然后,在任何可能使用普通 UIView的地方使用这个视图。配置它很简单,只需将 onlyRespondToTouchesInSubviews设置为 YES

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

这个解决方案比接受的答案要好一些,因为你仍然可以点击一个半透明的 UIView下的 UIButton,而 UIView的不透明部分仍然会响应触摸事件。

GIF

正如你在 GIF 中看到的,长颈鹿按钮是一个简单的矩形,但是透明区域上的触摸事件被传递到下面的黄色 UIButton

链接到课堂

以下是斯威夫特的版本:

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

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
}

基于 HitTest 的解决方案的 Swift 4实现

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

根据 Stuart 出色的、基本上是万无一失的回答,以及 Segev 有用的实现,这里有一个 Swift 4软件包,你可以把它放入任何项目:

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