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

![Xcode-13.1](https://img.shields.io/badge/Xcode-13.1-brightgreen) ![Swift-5.5.1](https://img.shields.io/badge/Swift-5.5.1-brightgreen) ![iOS-15.0](https://img.shields.io/badge/iOS-15.0-brightgreen) 

## はじめに
添付のような３階層（緑 -> 青 -> 赤）の View で赤をタップした場合は赤で処理、青をタップした場合は緑で処理をしたいということがありました。

<img width="380" alt="1" src="https://user-images.githubusercontent.com/34936885/152705698-53b3db12-2b27-492c-85d0-4b6666e00846.png">

タップイベントを透過させるにはなんとなく `hitTest` をオーバーライドすればいいんだろうなと思って下記のようにしたらムリだったのでなんとなく使ってた `hitTest` について調べました。

```swift
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` のドキュメント](https://developer.apple.com/documentation/uikit/uiview/1622469-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` の動作確認のために下記を実装します。

```swift
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 を配置してそれぞれカスタムクラスを設定します。

<img width="403" alt="2" src="https://user-images.githubusercontent.com/34936885/152705701-475f9188-6095-4e3e-bb42-fc781d03db7a.png">

## 動作確認
それぞれの 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` を返すように変更してみます。

```swift
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 の同階層もしくは１つ下の階層から探索が始まる模様。

* 緑タップ -> 青からはじまる
* 青タップ -> 橙からはじまる
* 赤タップ -> 橙からはじまる
* 橙タップ -> 赤からはじまる

返却値が `nil` の場合は対象 View の同階層もしくは１つ下の階層で 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` のログが出力されました。

```swift
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
}
```

## 結論
なぜ「はじめに」の下記実装ではだめなのか？

```swift
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` は発火しません。

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

上記を満たしたい場合、下記のように `hitTest` を `RedView` でオーバライドするのと `BlueView` オーバライドする２パターンが考えられます。

```swift
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
    }
}
```

```swift
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:

## 参考
* [hitTest：ドキュメント](https://developer.apple.com/documentation/uikit/uiview/1622469-hittest)