別のUIViewの下のUIViewとの相互作用を許可する


115

別のUIViewの下にあるUIViewのボタンとの相互作用を許可する簡単な方法はありますか-ボタンの上部にある上部のUIViewからの実際のオブジェクトはありませんか?

たとえば、現時点では、UIView(A)があり、画面の上部にオブジェクト、下部にオブジェクトがあり、中央には何もありません。これは、中央にボタンがある別のUIViewの上にあります(B)。ただし、Bの真ん中にあるボタンを操作できないようです。

Bのボタンを見ることができます-Aの背景をclearColorに設定しました-しかし、Bのボタンの上に実際にはAからのオブジェクトがないにもかかわらず、Bのボタンはタッチを受けていないようです。

編集 -私はまだトップUIViewのオブジェクトと対話できるようにしたい

確かにこれを行う簡単な方法はありますか?


2
そのほとんどすべてがここで説明されています:developer.apple.com/iphone/library/documentation/iPhone/… しかし、基本的に、hitTest:withEvent:をオーバーライドします。コードサンプルも提供されます。
ナッシュ

私はそのためだけに小さなクラスを書きました。(回答に例を追加)。UIButton半透明の下にあるをクリックしてUIViewも、透明部分がUIViewタッチイベントに応答するため、受け入れられる回答よりも少し良い解決策があります。
Segev 2015

回答:


97

トップビューの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:メソッドも確認できます。


19
ここで、middle_y1 / y2は何を表していますか?
Jason Renaldo 2014年

そのreturnステートメントが何をしているかはわかりませんがreturn CGRectContainsPoint(eachSubview.frame, point)、私にとってはうまくいきます。それ以外の場合は非常に役立つ回答
n00neimp0rtant

MIDDLE_Y1 / Y2ビジネスは単なる例です。この関数は、MIDDLE_Y1<=y<=MIDDLE_Y2エリア内のタッチイベントに対して「透過的」になります。
gyim

41

ここでの回答の多くは機能しますが、最も便利で一般的で間違いのない回答がここでは提供されていないことに少し驚いています。@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、このビューがタッチを受け取らないように指定します。レスポンダーチェーンが機能する方法は、このポイントより上のビュー階層が、タッチに応答するビューが見つかるまでトラバースされ続けることを意味します。スーパービューがタッチを受け付けるかどうかはこのビュー次第なので、スーパービューを返さないでください

このソリューションは次のとおりです。

  • 他のビュー/サブビュー/オブジェクトへの参照を必要としないため、便利です。
  • genericは、タッチ可能なサブビューのコンテナーとしてのみ機能するすべてのビューに適用され、サブビューの構成はそれが機能する方法に影響を与えません(オーバーライドpointInside:withEvent:して特定のタッチ可能な領域を返す場合のように)。
  • フールプルーフ、コードはそれほど多くありません...そしてその概念はあなたの頭を動かすことは難しくありません。

これを頻繁に使用して、サブクラスに抽象化し、1つのオーバーライドのために無意味なビューサブクラスを保存しました。おまけとして、プロパティを追加して構成可能にします。

@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。これを設定すると、設定するなど、単純なようであるonlyRespondToTouchesInSubviewsYES


2
唯一の正解。明確にするために、ビューへのタッチ無視したいが、ビューに含まれている(たとえば)ボタンへのタッチ無視したい場合は、Stuartが説明するようにしてください。(一部のボタンを無害に「保持」できるため、通常は「ホルダービュー」と呼びますが、ホルダーの「下」には影響を与えません。)
Fattie

UITableViewやUICollectionViewのようにビューがスクロール可能で、上または下にスクロールした場合、ポイントを使用する他のいくつかのソリューションでは問題が発生します。ただし、このソリューションはスクロールに関係なく機能します。
pajevic 2016

31

これを処理する方法はいくつかあります。私のお気に入りは、競合するビュー(これらをAとBと呼ぶような音)に共通のスーパービュー(多分間接的に)であるビューでhitTest:withEvent:をオーバーライドすることです。たとえば、次のようなものです(ここでAとBはUIViewポインターです。Bは「非表示」のポインターで、通常は無視されます)。

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

pointInside:withEvent:gyimが提案するようにメソッドを変更することもできます。これにより、少なくともタッチに対して、Aに効果的に「穴をあける」ことにより、本質的に同じ結果を得ることができます。

別のアプローチはイベント転送です。これは、オーバーライド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


このソリューションの問題-共通のスーパービューでhitTestをオーバーライドする-は、下のビューがスクロール可能なビューのセット全体(MapViewなど)の1つである場合、下のビューが完全に正しく機能しないことです。以下のgyimによって提案されているように、トップビューのpointInsideをオーバーライドすると、私が知る限りすべてのケースで機能します。
Delany、2011

@delany、それは真実ではありません。スクロール可能なビューを他のビューの下に配置して、hitTestをオーバーライドすることで、両方のビューを機能させることができます。これがサンプルコードです:bynomial.com/blogfiles/Temp32.zip
Tyler

ねえ。すべてのスクロール可能なビューではありません-一部だけ...上からあなたの郵便番号を試して、UIScrollViewを(たとえば)MKMapViewに変更しても動作しません。タップが機能する-問題と思われるのはスクロールです。
Delany、2011

さて、私はそれをチェックアウトし、hitTestがMKMapViewで望むように機能しないことを確認しました。@delany、あなたはそれについて正しかった。UIScrollViewで正しく動作しますが。MKMapViewが失敗するのはなぜですか?
タイラー

@Tylerおっしゃったように、「UIButtonのような組み込みコントロールは常に転送されたタッチを無視するということです」とおっしゃっていましたが、それを知っている方法と、この動作を説明する公式ドキュメントかどうかを知りたいのです。UIButtonにプレーンなUIViewサブビューがある場合、サブビューの境界内のタッチイベントに応答しないという問題に直面しました。そして、イベントがUIViewのデフォルトの動作として適切にUIButtonに転送されることがわかりましたが、それが指定された機能なのか、単なるバグなのかはわかりません。これに関するドキュメントを見せていただけますか?誠にありがとうございます。
Neal.Marlin

29

を設定する必要がありますupperView.userInteractionEnabled = NO;。そうしないと、上部のビューがタッチを遮断します。

これのInterface Builderバージョンは、「属性の表示」パネルの下部にある「ユーザー操作が有効」というチェックボックスです。チェックを外すと、問題ありません。


すみません-言ったはずです。私はまだ一番上のUIViewのオブジェクトと対話できるようにしたいです。
delany 2009年

しかし、upperViewはタッチを受け取れません。upperViewにボタンを含めてください。
キャプテン

2
この解決策は私にとってうまくいきます。タッチに反応するために上部のビューはまったく必要ありませんでした。
TJ

11

pointInside:withEvent:のカスタム実装は確かに進むべき道のように思えましたが、ハードコードされた座標を扱うことは私には奇妙に思えました。そのため、CGRectContainsPoint()関数を使用して、CGPointがボタンCGRect内にあるかどうかを確認しました。

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

8

最近、私はそれだけで私を助けるクラスを書きました。UIButtonまたはのカスタムクラスとして使用するとUIView、透明なピクセルで実行されたタッチイベントが渡されます。

UIButton半透明の下にあるをクリックしてUIViewも、透明な部分がUIViewタッチイベントに応答するため、この解決策は受け入れられた回答よりもいくらか優れています。

GIF

GIFで確認できるように、キリンボタンは単純な長方形ですが、透明な領域でのタッチイベントはUIButton下の黄色に渡されます。

クラスにリンク


2
コードはそれほど長くないため、将来プロジェクトが移動または削除された場合に備えて、コードに関連する部分を回答に含める必要があります。
Gavin

おかげで、あなたのソリューションは私のUIViewの非透明部分がタッチイベントに応答する必要がある場合に機能しますが、透明部分は機能しません。鮮やかさ!
Bruce

@ブルースグラッドそれはあなたを助けました!
セゲフ2015年

4

私はこのパーティーに少し遅れていると思いますが、この可能な解決策を追加します:

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

このコードを使用してカスタムUIViewの標準のhitTest関数をオーバーライドすると、ビュー自体のみが無視されます。そのビューのすべてのサブビューは通常どおりヒットを返します。ビュー自体に移動したはずのヒットは、そのスーパービューに渡されます。

-灰


1
これは私の好みの方法ですが、戻る必要はないと思います[self superview]。このメソッドのドキュメントには、「指定されたポイントを含むビュー階層(それ自体を含む)内のレシーバーの最も遠い子孫が返されます」と「ポイントがレシーバーのビュー階層の外に完全にある場合はnilが返されます」と記載されています。帰るべきだと思うnil。nilを返すと、スーパービューに制御が渡され、ヒットがあるかどうかがチェックされます。したがって、基本的には同じことを行いますが、スーパービューを返すと将来的に何かが壊れる可能性があります。
jasongregori 2012年

確かに、それはおそらく賢いでしょう(私の最初の回答の日付に注意してください-それ以来、より多くのコーディングを行いました)
Ash

4

受け入れられた回答をリフして、参照用にここに配置します。承認された回答は完全に機能します。このように拡張して、ビューのサブビューがタッチを受け取れるようにしたり、背後にあるビューに渡したりすることができます。

- (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:ん。各メソッドがそれを処理するためです。


3

userInteractionプロパティを無効に設定すると役立つ場合があります。例えば:

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

(注:上記のコードでは、「self」はビューを指します)

この方法では、topViewにのみ表示できますが、ユーザー入力は取得できません。これらのユーザーのタッチはすべてこのビューを介して行われ、下部のビューはそれらに対して応答します。このtopViewを使用して、透明な画像を表示したり、アニメーション化したりします。


3

このアプローチは非常にクリーンで、透明なサブビューがタッチに反応しないようにすることができます。サブクラス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

2

ここに私の解決策:

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

お役に立てれば


2

両方のビューでタッチを遮断するためにできることがいくつかあります。

上面図:

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

しかし、それはアイデアです。


これは確かに可能ですが、多くの作業が必要です(最下部のレイヤー(ボタンなど)で独自のタッチセンシティブオブジェクトをロールする必要があると思いますか?)。直感的に見える動作を取得する方法。
Delany 2009年

たぶん試してみるべきかなと思います。
アレクサンドルカサーニュ

2

これがSwiftバージョンです:

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

2

スウィフト3

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

1

UIツールキットを使用して完全なユーザーインターフェイスを構築したことがないので、これに関する経験はあまりありません。ここで私はうまくいくと思います。

すべてのUIView、およびこのUIWindowには、subviewsすべてのサブビューを含むNSArrayであるプロパティがあります。

ビューに追加する最初のサブビューはインデックス0を受け取り、次のインデックス1を受け取ります。また、置き換えることができaddSubview:insertSubview: atIndex:か、insertSubview:aboveSubview:階層内のあなたのサブビューの位置を決定することができるような方法及び。

コードをチェックして、UIWindowに最初に追加するビューを確認してください。それは0になり、もう
1は1になります。次に、サブビューの1つから別のサブビューに到達するには、次のようにします。

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

それがあなたのケースでうまくいくかどうか私に知らせてください!


(このマーカーの下は私の以前の答えです):

ビューが相互に通信する必要がある場合は、コントローラーを介して(つまり、一般的なMVCを使用して)通信する必要があります。モデル)。

新しいビューを作成するとき、それがコントローラーに登録されることを確認できます。

したがって、この手法は、ビューをコントローラーに登録することです(コントローラーは、名前または辞書または配列で好きなものを使用してビューを格納できます)。コントローラーにメッセージを送信させるか、ビューへの参照を取得して直接通信することができます。

ビューにコントローラーへのリンクがない場合(そうである可能性があります)、シングルトンまたはクラスメソッド、あるいはその両方を使用して、コントローラーへの参照を取得できます。


お返事ありがとうございます。しかし、よくわかりません。どちらのビューにもコントローラーがあります-問題は、1つのビューが他のビューの上にある場合、実際にはそれらのイベントを「ブロック」するオブジェクトがない場合でも、下のビューがイベントを取得していない(そしてそれらをコントローラーに転送していない)ことです。上面図。
Delany、2009年

UIViewの上部にUIWindowがありますか?もしそうなら、イベントは伝播されるべきであり、「魔法」を行う必要はないはずです。Apple Dev Centerで[ウィンドウとビュー] [1]を読んでください(これが役に立たない場合は、ぜひコメントを追加してください!)[1]:developer.apple.com/iphone/library/documentation/ iPhone /…
ナッシュ

はい、絶対に-階層の最上位にあるUIWindow。
delany 2009年

回答を更新して、ビューの階層を移動するためのコードを含めました。他にサポートが必要な場合はお知らせください。
ナッシュ

助けてくれてありがとう-私はこれらのインターフェースを構築するのにかなりの経験があり、私の現在のインターフェースは私の知る限り正しく設定されています。問題は、他のビューの上に配置された場合のフルビューのデフォルトの動作のようです。
delany 2009年

1

ビュー階層に組み込まれたビューチェーンを使用するのが正しい方法だと思います。メインビューにプッシュされるサブビューについては、ジェネリックUIViewを使用せずに、UIView(またはUIImageViewなどのバリアントの1つ)をサブクラス化してMYView:UIView(またはUIImageViewなどの任意のスーパータイプ)にします。YourViewの実装で、touchesBeganメソッドを実装します。このメソッドは、そのビューがタッチされると呼び出されます。その実装で必要なのはインスタンスメソッドだけです。

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

このtouchesBeganはレスポンダーAPIであるため、パブリックインターフェイスまたはプライベートインターフェイスで宣言する必要はありません。これは、知っておく必要がある魔法のapiの1つです。このself.superviewは、リクエストを最終的にviewControllerにバブルします。次に、viewControllerで、このtouchesBeganを実装してタッチを処理します。

タッチ位置(CGPoint)は、ビュー階層チェーンで跳ね返るときに、周囲のビューに対して自動的に調整されることに注意してください。


1

これを投稿したいのですが、私には多少似た問題があり、かなりの時間を費やして、運がなくてもここで回答を実装しようとしました。私がやったこと:

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

と実装UIGestureRecognizerDelegate

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

下のビューは、多くのセグエを備えたナビゲーションコントローラーで、パンジェスチャーで閉じることができるドアがその上にありました。さらに別のVCにすべてが埋め込まれました。魅力のように働いた。お役に立てれば。



0

スチュアートの優れた、ほとんど誰にでもできる答えと、セゲフの便利な実装から導き出されたものは、任意のプロジェクトにドロップできる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
    }
}

そして、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)
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.