2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Viewのタップイベントを透過させる(Swift)

Posted at

Xcode-13.1 Swift-5.5.1 iOS-15.0

はじめに

添付のような3階層(緑 -> 青 -> 赤)の View で赤をタップした場合は赤で処理、青をタップした場合は緑で処理をしたいということがありました。

1

タップイベントを透過させるにはなんとなく 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 を配置してそれぞれカスタムクラスを設定します。

2

動作確認

それぞれの 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 を探索する模様。

動作確認のログの最後を見ると、GreenViewhitTest の返却値の 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 である GreenViewhitTest で返却される 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
    }
}

上記の場合、青をタップしたときに GreenViewhitTest で返却される値は nil になるのでどの View でも touchesBegan は発火しません。

3階層(緑 -> 青 -> 赤)の View で赤をタップした場合は赤で処理、青をタップした場合は緑で処理をしたいということがありました。

上記を満たしたい場合、下記のように hitTestRedView でオーバライドするのと 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
    }
}

これで青部分だけタップを透過させることができました:raised_hands:

おわりに

なんとなく使ってた hitTest について今回の調査でより理解が深まった気がします:muscle:

ただ、ドキュメントをうまく探せなかったのでこの処理で本当にいいのかはちょっとわかりません。。。:joy_cat:

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?