私の問題:基本的にアプリケーションフレーム全体を占めるスーパービューEditView
と、下側のみを占めるサブビューMenuView
〜20%があります。 MenuView
には独自のサブビューButtonView
が含まれていますが、これは実際にはMenuView
の境界の外側にあります(次のようなものです:ButtonView.frame.Origin.y = -100
)。
(注:EditView
には、MenuView
のビュー階層の一部ではない他のサブビューがありますが、答えに影響する場合があります。)
あなたはおそらくすでに問題を知っています:ButtonView
がMenuView
の境界内にあるとき(または、より具体的には、私のタッチがMenuView
の境界内にあるとき)、ButtonView
タッチイベントに応答します。タッチがMenuView
の境界の外側にある(ただし、ButtonView
の境界内にある)場合、ButtonView
はタッチイベントを受け取りません。
例:
EditView
ですMenuView
、EditViewのサブビューですButtonView
、MenuViewのサブビューです図:
+------------------------------+
|E |
| |
| |
| |
| |
|+-----+ |
||B | |
|+-----+ |
|+----------------------------+|
||M ||
|| ||
|+----------------------------+|
+------------------------------+
(B)は(M)のフレームの外側にあるため、(B)領域のタップは(M)に送信されません-実際、この場合、(M)はタッチを分析せず、タッチは階層内の次のオブジェクト。
目標:オーバーライドhitTest:withEvent:
はこの問題を解決できますが、どのように正確に理解していないのですか。私の場合、hitTest:withEvent:
EditView
(私の「マスター」スーパービュー)でオーバーライドされますか?または、タッチを受信していないボタンの直接のスーパービューであるMenuView
でオーバーライドする必要がありますか?または私はこれについて間違って考えていますか?
これに長い説明が必要な場合は、適切なオンラインリソースが役立ちます-AppleのUIViewドキュメントを除きます。
ありがとう!
受け入れられた回答のコードをより一般的なものに変更しました-ビューがその境界にサブビューをクリップする場合、非表示になる場合、さらに重要な場合を処理します:サブビューが複雑なビュー階層の場合、正しいサブビューが返されます.
- (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
}
これが、より複雑なユースケースでこのソリューションを使用しようとしている人に役立つことを願っています。
OK、私はいくつかの掘り下げとテストを行いました。ここにhitTest:withEvent
の仕組みがあります-少なくとも高レベルで。このシナリオをイメージしてください:
図:
+------------------------------+
|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];
}
私はまだこの方法とこの問題に非常に新しいので、コードを記述するより効率的または正しい方法があれば、コメントしてください。
後でこの質問に答えた他の人に役立つことを願っています。 :)
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
}
私がやることは、ButtonViewとMenuViewの両方を、フレームが両方に完全に適合するコンテナに両方を配置することにより、ビュー階層の同じレベルに存在させることです。このように、クリップされたアイテムのインタラクティブな領域は、スーパービューの境界のために無視されません。
親ビュー内に他の多くのサブビューがある場合、上記のソリューションを使用するとおそらく他のインタラクティブビューのほとんどが機能しません。その場合、次のようなものを使用できます(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)
}
}
誰かがそれを必要とするなら、ここに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
}