SwiftUIでページ遷移(左部)を実装する

# 参考
[【iOS】遷移方法まとめとエンジニア1年生の独り言](https://qiita.com/iTakahiro/items/b33306c773a05845358f)
[上部左右から出現するメニューを作る](https://d1v1b.com/swiftui/various_slidemenu)
[SwiftUIのGestureについてまとめ](https://kuro-46.hatenablog.com/entry/2019/10/30/064929)

# 画面遷移について
1. モーダル ... 下から画面が出現
2. プッシュ ... 右から画面が出現
3. タブ ... 瞬時の切り替わり(タブアイテムのタップ)
4. ページ ... 左右から画面が出現(左右スワイプ) ※サイドメニューと同義

# ページ遷移(左部)の実装
## 動画
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ページ遷移(左部) <a href="https://t.co/0uWsH7ef0R">pic.twitter.com/0uWsH7ef0R</a></p>&mdash; APPLE (@applecoder6) <a href="https://twitter.com/applecoder6/status/1381934696829841410?ref_src=twsrc%5Etfw">April 13, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## コード
```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var showingModal = false
    
    @State private var vOffset = CGFloat.zero    // 現在のオフセット(y軸方向)
    @State private var vCloseOffset = CGFloat.zero   // 閉状態のオフセット(y軸方向)
    @State private var vOpenOffset = CGFloat.zero    // 開状態のオフセット(y軸方向)
    @State private var hOffset = CGFloat.zero    // 現在のオフセット(x軸方向)
    @State private var hCloseOffset = CGFloat.zero   // 閉状態のオフセット(x軸方向)
    @State private var hOpenOffset = CGFloat.zero    // 開状態のオフセット(x軸方向)
   
    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                // メインコンテンツ
                VStack {
                    Button("Show ModalView.") {
                        self.showingModal.toggle()
                    }.sheet(isPresented: $showingModal) {
                        ModalView()
                    }
                    
                    Divider()
                    
                    NavigationView {
                        NavigationLink(destination: PushView()) {
                            Text("Show PushView.")
                        }.navigationBarTitle("ContentView", displayMode: .automatic)
                    }
                    
                    Divider()
                    
                    TabView {
                        TabAView()
                            .tabItem {
                                Image(systemName: "moon")
                                Text("Tab A")
                            }
                        
                        TabBView()
                            .tabItem {
                                Image(systemName: "moon.fill")
                                Text("Tab B")
                            }
                    }
                }
                // 上部メニュー
                PageUpView()
                    .foregroundColor(Color.red)
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    // View描画時に呼び出すメソッド
                    .onAppear(perform: {
                        // PageUpViewのオフセット初期値として負の方向(上方向)に[Safe Areaの高さ]+[PageUpView自体の高さ]分ずらす
                        self.vOffset = -1 * (geometry.frame(in: .global).origin.y + geometry.size.height)
                        // 閉状態のオフセット = 初期状態のオフセット
                        self.vCloseOffset = self.vOffset
                        // 開状態のオフセット = 0
                        self.vOpenOffset = .zero
                    })
                    // オフセット
                    .offset(y: self.vOffset)
                    // アニメーション
                    .animation(.default)
                
                // 左部メニュー
                PageLeftView()
                    .foregroundColor(Color.blue)
                    .frame(width: geometry.size.width)
                    .edgesIgnoringSafeArea(.all)
                    // View描画時に呼び出すメソッド
                    .onAppear(perform: {
                        self.hOffset = -1 * geometry.size.width
                        self.hCloseOffset = self.hOffset
                        self.hOpenOffset = .zero
                    })
                    .offset(x: self.hOffset)
                    .animation(.default)
            }
            //ジェスチャーに関する実装
            .gesture(DragGesture(minimumDistance: 5)
                        // DragGestureの入力開始時の呼び出しメソッド
                        .onChanged { value in
                            // ContentViewのy軸方向の現在のオフセットが初期値でない かつ DragGestureの開始位置のy座標が30未満の場合
                            if (self.vOffset != self.vOpenOffset && value.startLocation.y < 30) {
                                // ContentViewの現在のオフセット = 初期状態からy軸方向にDragされた距離
                                self.vOffset = self.vCloseOffset + value.translation.height
                            }
                            // ContentViewのx軸方向の現在のオフセットが0未満(常に満たす) かつ DragGestureの開始位置のx座標が30未満の場合
                            else if (self.hOffset < self.hOpenOffset && value.startLocation.x < 30) {
                                // ContentViewの現在のオフセット = 初期値からx方向にDragされた距離
                                self.hOffset = self.hCloseOffset + value.translation.width
                            }
                        }
                        // DragGestureの入力終了時の呼び出しメソッド
                        .onEnded { value in
                            //Gesture終了時のViewのy座標がGesture開始時よりも高い(Viewが下方向に動いた場合)
                            if (value.startLocation.y < value.location.y) {
                                // Gestureの開始位置のy座標が30未満の場合
                                if (value.startLocation.y < 30) {
                                    // PageUpViewを開状態にする
                                    self.vOffset = self.vOpenOffset
                                }
                            }
                            // Gesture終了時のViewのx座標がGesture開始時よりも高い(Viewが右方向に動いた場合)
                            else if (value.location.x > value.startLocation.x) {
                                if (value.startLocation.x < 30) {
                                    // PageLeftViewを開状態にする
                                    self.hOffset = self.hOpenOffset
                                }
                            }
                            else {
                                // PageUpView, PageLeftViewを閉状態にする
                                self.hOffset = self.hCloseOffset
                                self.vOffset = self.vCloseOffset
                            }
                        }
            )
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

```swift:PageLeftView.swift
import SwiftUI

struct PageLeftView: View {
    var body: some View {
        ZStack {
            Color.blue
            Text("This is Left Side.")
        }
    }
}

struct PageLeftView_Previews: PreviewProvider {
    static var previews: some View {
        PageLeftView()
    }
}
```

# 学習結果
## オフセット変数
offset変数はContentView自身で管理している、ContentViewのオフセットを指す。
上下左右のサイドメニューを実装する際はx軸/y軸方向それぞれにオフセット変数を設定する必要がある。

## 不具合
DragGesture時にPageLeftViewが完全な開状態にならず、
スライドした分だけで画面が止まってしまう場合がある。(動画の冒頭参照)
 → 考察: Gesture時に、`onChanged`修飾子内の`self.hOffset = self.hCloseOffset + value.translation.width`が動作している?