はじめに
添付のような3階層(緑 -> 青 -> 赤)の View で赤をタップした場合は赤で処理、青をタップした場合は緑で処理をしたいということがありました。
タップイベントを透過させるにはなんとなく hitTest
をオーバーライドすればいいんだろうなと思って下記のようにしたらムリだったのでなんとなく使ってた hitTest
について調べました。
class GreenView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView is RedView || hitView == self {
return hitView
}
return nil
}
}
hitTest について
hitTest
のドキュメントをみると下記のように書いてあります。
This method traverses the view hierarchy by calling the point(inside:with:) method of each subview to determine which subview should receive a touch event.
このメソッドでどの View がタップイベントを処理するか決めているようです。
ただドキュメントをみただけだと実際どんな感じになるのかわからなかったので動作を確認してみます。
準備
hitTest
の動作確認のために下記を実装します。
class GreenView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
print("hitTest!!! Green, hit: \(hitView?.className ?? "nil")")
return hitView
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesBegan!!! Green")
}
}
class BlueView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
print("hitTest!!! Blue, hit: \(hitView?.className ?? "nil")")
return hitView
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesBegan!!! Blue")
}
}
class RedView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
print("hitTest!!! Red, hit: \(hitView?.className ?? "nil")")
return hitView
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesBegan!!! Red")
}
}
class OrangeView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
print("hitTest!!! Orange, hit: \(hitView?.className ?? "nil")")
return hitView
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesBegan!!! Orange")
}
}
extension UIView {
var className: String {
return String(describing: type(of: self))
}
}
Storyboard で下記のように View を配置してそれぞれカスタムクラスを設定します。
動作確認
それぞれの View をタップした場合の処理をみていきます。
緑をタップ
緑の部分をタップした場合、以下のようにログが表示されます。
hitTest!!! Blue, hit: nil
hitTest!!! Blue, hit: nil
hitTest!!! Blue, hit: nil
hitTest!!! Green, hit: GreenView
hitTest!!! Blue, hit: nil
hitTest!!! Green, hit: GreenView
touchesBegan!!! Green
青をタップ
青い部分をタップした場合、以下のようにログが表示されます。
hitTest!!! Orange, hit: nil
hitTest!!! Red, hit: nil
hitTest!!! Orange, hit: nil
hitTest!!! Red, hit: nil
hitTest!!! Orange, hit: nil
hitTest!!! Red, hit: nil
hitTest!!! Blue, hit: BlueView
hitTest!!! Green, hit: BlueView
hitTest!!! Orange, hit: nil
hitTest!!! Red, hit: nil
hitTest!!! Blue, hit: BlueView
hitTest!!! Green, hit: BlueView
touchesBegan!!! Blue
赤をタップ
赤い部分をタップした場合、以下のようにログが表示されます。
hitTest!!! Orange, hit: nil
hitTest!!! Orange, hit: nil
hitTest!!! Orange, hit: nil
hitTest!!! Red, hit: RedView
hitTest!!! Blue, hit: RedView
hitTest!!! Green, hit: RedView
hitTest!!! Orange, hit: nil
hitTest!!! Red, hit: RedView
hitTest!!! Blue, hit: RedView
hitTest!!! Green, hit: RedView
touchesBegan!!! Red
橙をタップ
橙色の部分をタップした場合、以下のようにログが表示されます。
hitTest!!! Red, hit: nil
hitTest!!! Red, hit: nil
hitTest!!! Orange, hit: OrangeView
hitTest!!! Blue, hit: OrangeView
hitTest!!! Green, hit: OrangeView
hitTest!!! Orange, hit: OrangeView
hitTest!!! Blue, hit: OrangeView
hitTest!!! Green, hit: OrangeView
touchesBegan!!! Orange
橙を無視する
OrangeView
の実装を以下のように nil
を返すように変更してみます。
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
print("hitTest!!! Orange, hit: \(hitView?.className ?? "nil")")
return nil
}
この状態で橙色の部分をタップすると以下のようなログになります。
hitTest!!! Red, hit: nil
hitTest!!! Red, hit: nil
hitTest!!! Orange, hit: OrangeView
hitTest!!! Red, hit: nil
hitTest!!! Blue, hit: BlueView
hitTest!!! Green, hit: BlueView
hitTest!!! Orange, hit: OrangeView
hitTest!!! Red, hit: nil
hitTest!!! Blue, hit: BlueView
hitTest!!! Green, hit: BlueView
touchesBegan!!! Blue
動作確認の考察
動作確認のログの最初を見ると、下記のようにタップした View の同階層もしくは1つ下の階層から探索が始まる模様。
- 緑タップ -> 青からはじまる
- 青タップ -> 橙からはじまる
- 赤タップ -> 橙からはじまる
- 橙タップ -> 赤からはじまる
返却値が nil
の場合は対象 View の同階層もしくは1つ下の階層で View を探索する模様。
動作確認のログの最後を見ると、GreenView
の hitTest
の返却値の touchesBegan
が発火する模様。
-
緑タップ
hitTest!!! Green, hit: GreenView touchesBegan!!! Green
-
青タップ
hitTest!!! Green, hit: BlueView touchesBegan!!! Blue
-
赤タップ
hitTest!!! Green, hit: RedView touchesBegan!!! Red
-
橙タップ
hitTest!!! Green, hit: OrangeView touchesBegan!!! Orange
-
橙タップ(橙は無視)
hitTest!!! Green, hit: BlueView touchesBegan!!! Blue
今回の場合の最上位 View である GreenView
の hitTest
で返却される View がタップイベントを処理する View になる模様。
試しに GreenView
の実装を下記のようにするとどこをタップしても touchesBegan!!! Green
のログが出力されました。
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
print("hitTest!!! Green, hit: \(hitView?.className ?? "nil")")
return self
}
結論
なぜ「はじめに」の下記実装ではだめなのか?
class GreenView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView is RedView || hitView == self {
return hitView
}
return nil
}
}
上記の場合、青をタップしたときに GreenView
の hitTest
で返却される値は nil
になるのでどの View でも touchesBegan
は発火しません。
3階層(緑 -> 青 -> 赤)の View で赤をタップした場合は赤で処理、青をタップした場合は緑で処理をしたいということがありました。
上記を満たしたい場合、下記のように hitTest
を RedView
でオーバライドするのと BlueView
オーバライドする2パターンが考えられます。
class GreenView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView is RedView {
return hitView
}
return self
}
}
class BlueView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView is RedView {
return hitView
}
return nil
}
}
これで青部分だけタップを透過させることができました
おわりに
なんとなく使ってた hitTest
について今回の調査でより理解が深まった気がします
ただ、ドキュメントをうまく探せなかったのでこの処理で本当にいいのかはちょっとわかりません。。。