web-dev-qa-db-ja.com

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

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

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

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

例:

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

図:

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

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

目標:オーバーライドhitTest:withEvent:はこの問題を解決できますが、どのように正確に理解していないのですか。私の場合、hitTest:withEvent:EditView(私の「マスター」スーパービュー)でオーバーライドされますか?または、タッチを受信して​​いないボタンの直接のスーパービューであるMenuViewでオーバーライドする必要がありますか?または私はこれについて間違って考えていますか?

これに長い説明が必要な場合は、適切なオンラインリソースが役立ちます-AppleのUIViewドキュメントを除きます。

ありがとう!

89
toblerpwn

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

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

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
}

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

137
Noam

OK、私はいくつかの掘り下げとテストを行いました。ここにhitTest:withEventの仕組みがあります-少なくとも高レベルで。このシナリオをイメージしてください:

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

図:

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

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

後でこの質問に答えた他の人に役立つことを願っています。 :)

31
toblerpwn

In 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
}
22
duan

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

2
mamackenzie

親ビュー内に他の多くのサブビューがある場合、上記のソリューションを使用するとおそらく他のインタラクティブビューのほとんどが機能しません。その場合、次のようなものを使用できます(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
paras gupta

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

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
Sonny