SwiftUIにおけるジェスチャ制御：highPriorityGestureを使ってPickerの動作を保護する

# はじめに

SwiftUIを用いてUIを設計する際、特定の要素（ここではPicker）の上でジェスチャ（TapGesture）を適切に制御することは重要な課題となります。本記事では、特にhighPriorityGesture(_:)メソッドを使ってPickerの選択を妨げないようにする方法を解説します。

## 基本的なジェスチャ制御

まず、基本的なジェスチャ制御について簡単に紹介します。以下のコードを見てみましょう。

```Swift
Rectangle()
    .fill(Color.red)
    .frame(width: 200, height: 200)
    .gesture(
        TapGesture()
            .onEnded { _ in print("Tap detected") }
    )
```
このコードでは、赤い四角形のRectangleビューが表示され、このビューをタップすると "Tap detected" がコンソールに出力されます。これはTapGestureがこのビューにアタッチされ、タップが終了したときにクロージャが実行されるからです。

## ジェスチャの競合

しかし、1つのビューに複数のジェスチャを適用した場合、どのジェスチャが優先されるかが問題となります。これを解決するのがhighPriorityGesture(_:)メソッドです。

```Swift
Rectangle()
    .fill(Color.red)
    .frame(width: 200, height: 200)
    .highPriorityGesture(
        DragGesture()
            .onChanged { _ in print("Drag detected") }
    )
    .gesture(
        TapGesture()
            .onEnded { _ in print("Tap detected") }
    )
```

この例では、DragGestureがhighPriorityGesture(_:)で設定されていて、TapGestureが通常のgesture(_:)で設定されています。これにより、ドラッグとタップが競合する場合にはドラッグが優先されます。

## Pickerとジェスチャの制御

では、具体的な問題例として、Pickerを含むビュー全体にタップジェスチャが適用されていて、Pickerの選択を妨げてしまうケースを考えてみましょう。以下のようなコードがあったとします。

```Swift
struct ContentView: View {
    @State private var viewState: Int = 0
    @State private var selectionValue:Int  = 0
    
    var body: some View {
        ZStack {
            if viewState == 0 {
                Color.red
            } else if viewState == 1 {
                Color.blue
            } else {
                Color.green
            }
            
            if (viewState == 0) {
                Picker("", selection: $selectionValue) {
                    Text("test1").tag(0)
                    Text("test2").tag(1)
                }
                .border(Color.white, width: 2)
            }
        }
        .edgesIgnoringSafeArea(.all)
        .onTapGesture {
            withAnimation {
                viewState = (viewState + 1) % 3
            }
        }
    }
}
```

全体のビューにタップジェスチャが適用されており、タップすると赤→青→緑という順番で背景色が変わっていきます。また、Pickerは背景色が赤の時のみ、表示されます。しかし、この実装ではPickerの選択がうまくいきません。なぜなら、Pickerを選択しようとするとビュー全体のタップジェスチャが発火し、結果として背景色が青になりPickerが閉じてしまうからです。


<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1923880/d689d34b-6c1a-b909-f377-013fcdf79c91.gif">

## 高優先度ジェスチャを利用する

これを解決するために、highPriorityGesture(_:)を用いてPicker上のタップジェスチャを無視させます。次のようにコードを変更します。

```Swift
struct ContentView: View {
    @State private var viewState: Int = 0
    @State private var selectionValue:Int  = 0
    
    var body: some View {
        ZStack {
            if viewState == 0 {
                Color.red
            } else if viewState == 1 {
                Color.blue
            } else {
                Color.green
            }
            
            if (viewState == 0) {
                Picker("", selection: $selectionValue) {
                    Text("test1").tag(0)
                    Text("test2").tag(1)
                }
                .border(Color.white, width: 2)
                // highPriorityGestureを追加
                .highPriorityGesture(
                    TapGesture().onEnded { _ in
                        // do nothing
                    }
                )
            }
        }
        .edgesIgnoringSafeArea(.all)
        .onTapGesture {
            withAnimation {
                viewState = (viewState + 1) % 3
            }
        }
    }
}
```

このようにすると、Picker上でのタップは高優先度ジェスチャとして認識され、onEndedで何も行わないためPickerの選択は妨げられません。一方、Picker以外の部分でのタップは全体のビューに適用されているonTapGestureによって、次の背景色になります。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1923880/ca641efc-0a45-43f5-b15c-7db25a17009e.gif">

## まとめ

この記事では、SwiftUIのhighPriorityGesture(_:)を使って、Pickerの選択を妨げないようにする方法について紹介しました。これは、特定のビューで特定のジェスチャを保護する際に非常に便利な手法です。ジェスチャの制御は、ユーザーインターフェースを設計する上で重要な考慮事項であり、SwiftUIはこのような繊細な制御を可能にします。

参考
https://developer.apple.com/documentation/swiftui/view/highprioritygesture(_:including:)