hitTest:withEventを使用して、スーパービューのフレームの外側にあるサブビューにタッチをキャプチャする:


94

私の問題:EditView基本的にアプリケーションフレーム全体を占めるスーパービューとMenuView、下部〜20%だけを占めるMenuViewサブビューがButtonViewあり、実際にはMenuViewの境界外にある独自のサブビューが含まれています(このようなもの:)ButtonView.frame.origin.y = -100

(注:のビュー階層のEditView一部ではない他のサブビューMenuViewがありますが、回答に影響する可能性があります。)

あなたはおそらくすでに問題を知っています:ButtonViewがの範囲内にある場合MenuView(または、より具体的には、私のタッチがの範囲内にある場合MenuViewButtonViewは、タッチイベントに応答します。タッチがMenuViewの範囲外(ただしButtonViewの範囲内)にある場合、はタッチイベントを受け取りませんButtonView

例:

  • (E)is EditView、すべてのビューの親
  • (M)is MenuView、EditViewのサブビュー
  • (B)is ButtonView、MenuViewのサブビュー

図:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

(B)は(M)のフレームの外にあるため、(B)領域のタップは(M)に送信されません-実際、(M)はこの場合タッチを分析せず、タッチは階層内の次のオブジェクト。

目標:オーバーライドhitTest:withEvent:することでこの問題を解決できることを確認しましたが、正確な方法がわかりません。私の場合、(私の「マスター」スーパービュー)hitTest:withEvent:でオーバーライドする必要がありEditViewますか?またはそれをオーバーライドする必要がありますMenuView、タッチを受け取らないボタンの直接のスーパービューでがありますか?それとも私はこれについて間違って考えていますか?

これに長い説明が必要な場合は、優れたオンラインリソースが役立ちます。ただし、AppleのUIViewのドキュメントは例外で、私には明確にされていません。

ありがとう!

回答:


145

受け入れられた回答のコードをより一般的なものに変更しました-これは、ビューがサブビューをその境界にクリップするケースを処理し、非表示になる場合があり、さらに重要なことに、サブビューが複雑なビュー階層である場合、正しいサブビューが返されます。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (self.clipsToBounds) {
        return nil;
    }

    if (self.hidden) {
        return nil;
    }

    if (self.alpha == 0) {
        return nil;
    }

    for (UIView *subview in self.subviews.reverseObjectEnumerator) {
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        UIView *result = [subview hitTest:subPoint withEvent:event];

        if (result) {
            return result;
        }
    }

    return nil;
}

SWIFT 3

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

    if clipsToBounds || isHidden || alpha == 0 {
        return nil
    }

    for subview in subviews.reversed() {
        let subPoint = subview.convert(point, from: self)
        if let result = subview.hitTest(subPoint, with: event) {
            return result
        }
    }

    return nil
}

これが、このソリューションをより複雑なユースケースで使用しようとしている人にとって役立つことを願っています。


これは見栄えがよく、実行していただきありがとうございます。ただし、1つの質問がありました:なぜreturn [super hitTest:point withEvent:event]; ?タッチをトリガーするサブビューがない場合は、単にnilを返さないでしょうか?Appleは、サブビューにタッチが含まれていない場合、hitTestはnilを返すと述べています。
Ser Pounce

うーん、そうですね。また、正しい動作をさせるには、オブジェクトを逆の順序で反復する必要があります(最後のオブジェクトが視覚的に一番上にあるため)。一致するようにコードを編集しました。
ノーム2013年

3
私はあなたのソリューションを使用して、UIButtonにタッチをキャプチャさせました。これは、UICollectionView内(明らかに)のUICollectionViewCell内にあるUIView内にあります。この3つのクラスでhitTest:withEvent:をオーバーライドするには、UICollectionViewとUICollectionViewCellをサブクラス化する必要がありました。そしてそれは魅力のように動作します!! ありがとう!!
ダニエルガルシア

3
使用目的に応じて、[super hitTest:point withEvent:event]またはnilを返す必要があります。自己を返すと、すべてを受け取ることになります。
ノーム14年

1
これと同じテクニックに関するAppleのQ&Aテクニカルドキュメントは、次のとおりです
。developer.apple.com/library/ios/qa/qa2013/qa1812.html

33

さて、私はいくつかの掘り起こしとテストを行いました、これがどのようにhitTest:withEvent機能するかです-少なくとも高いレベルで。このシナリオをイメージしてください:

  • (E)は、すべてのビューの親であるEditViewです
  • (M)はEditViewのサブビューであるMenuViewです。
  • (B)はMenuViewのサブビューであるButtonViewです。

図:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

(B)は(M)のフレームの外にあるため、(B)領域のタップは(M)に送信されません-実際、(M)はこの場合タッチを分析せず、タッチは階層内の次のオブジェクト。

ただし、hitTest:withEvent:(M)で実装した場合、アプリケーション内の任意の場所のタップが(M)に送信されます(または、少なくともタップについて認識しています)。その場合にタッチを処理するコードを記述して、タッチを受け取るオブジェクトを返すことができます。

より具体的にhitTest:withEvent:は、の目標は、ヒットを受け取るオブジェクトを返すことです。したがって、(M)では、次のようなコードを書くことができます。

// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
    for (UIView *subview in self.subviews) {
        if (CGRectContainsPoint(subview.frame, point)) {
            return subview;
        }
    }

    // use this to pass the 'touch' onward in case no subviews trigger the touch
    return [super hitTest:point withEvent:event];
}

私はまだこの方法とこの問題に非常に新しいので、コードを書くためのより効率的または正しい方法がある場合は、コメントしてください。

それが後でこの質問にぶつかった人を助けることを願っています。:)


おかげで、これでうまくいきましたが、実際にヒットを受け取るサブビューを決定するために、さらにロジックを実行する必要がありました。私はあなたの例から不要なブレークを削除しました。
Daniel Saidi、2014

サブビューフレームが親ビューの境界フレームである場合、クリックイベントが応答しませんでした。あなたの解決策はこの問題を修正しました。
どうも

@toblerpwn(面白いニックネーム:))この優れた古い回答を編集して、どのクラスにこれを追加する必要があるかを非常に明確にする必要があります(USE LARGE BOLD CAPS)。乾杯!
Fattie

26

Swift 5で

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
    for member in subviews.reversed() {
        let subPoint = member.convert(point, from: self)
        guard let result = member.hitTest(subPoint, with: event) else { continue }
        return result
    }
    return nil
}

これは、「誤動作」ビューの直接のスーパービューにオーバーライドを適用した場合にのみ機能します
Hudi Ilfeld

2

私がやろうとしていることは、ButtonViewとMenuViewの両方をビュー階層の同じレベルに存在させることです。それらは両方ともフレームに完全に適合するコンテナに両方を配置することによってです。このように、スーパービューの境界があるため、クリップされたアイテムのインタラクティブ領域は無視されません。


...それは、私はいくつかの配置ロジックを複製する必要があることを意味しますが、それは確かに最後に私の最良の選択であり(またはいくつかの深刻なコードをリファクタリング!) -私も、この回避策について考えた
toblerpwn

1

親ビュー内に他の多くのサブビューがある場合、おそらく他のインタラクティブビューのほとんどは、上記のソリューションを使用すると機能しません。その場合、次のようなものを使用できます(In Swift 3.2):

class BoundingSubviewsViewExtension: UIView {

    @IBOutlet var targetView: UIView!

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview
        let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
        if (targetView?.bounds.contains(pointForTargetView!))! {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
        }
        return super.hitTest(point, with: event)
    }
}

0

誰かがそれを必要とするなら、ここに迅速な代替があります

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    if !self.clipsToBounds && !self.hidden && self.alpha > 0 {
        for subview in self.subviews.reverse() {
            let subPoint = subview.convertPoint(point, fromView:self);

            if let result = subview.hitTest(subPoint, withEvent:event) {
                return result;
            }
        }
    }

    return nil
}

0

以下のコード行をビュー階層に配置します。

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

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

より明確にするために、それは私のブログで説明されていました:「カスタムコールアウト:ボタンがクリックできない問題」に関する「goaheadwithiphonetech」。

お役に立てば幸いです... !!!


あなたのブログは削除されました。説明はどこにありますか?
ishahak 2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.