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. ページ ... 左右から画面が出現(左右スワイプ) ※サイドメニューと同義

# ページ遷移(上部)の実装
## 写真
![IMG_0954.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/4a50f004-0c2c-7867-5ec4-ab1113e00370.jpeg)


## 動画
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ページ遷移(上部) <a href="https://t.co/UMy5vxnxM4">pic.twitter.com/UMy5vxnxM4</a></p>&mdash; APPLE (@applecoder6) <a href="https://twitter.com/applecoder6/status/1381643213891530759?ref_src=twsrc%5Etfw">April 12, 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 offset = CGFloat.zero    // 現在のオフセット
    @State private var closeOffset = CGFloat.zero   // 閉状態のオフセット
    @State private var openOffset = CGFloat.zero    // 開状態のオフセット
   
    var body: some View {
        GeometryReader { geometry in
            ZStack {
                // メインコンテンツ
                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()
                    .background(Color.gray)
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    // View描画時に呼び出すメソッド
                    .onAppear(perform: {
                        // PageUpViewのオフセット初期値として負の方向(上方向)に[Safe Areaの高さ]+[PageUpView自体の高さ]分ずらす
                        self.offset = -1 * (geometry.frame(in: .global).origin.y + geometry.size.height)
                        // 閉状態のオフセット = 初期状態のオフセット
                        self.closeOffset = self.offset
                        // 開状態のオフセット = 0
                        self.openOffset = .zero
                    })
                    // オフセット
                    .offset(y: self.offset)
                    // アニメーション
                    .animation(.default)
            }
            //ジェスチャーに関する実装
            .gesture(DragGesture(minimumDistance: 5)
                        // DragGestureの入力開始時の呼び出しメソッド
                        .onChanged { value in
                            // ContentViewの現在のオフセットが初期値でない かつ DragGestureの開始位置のy座標が30未満の場合
                            if (self.offset != self.openOffset && value.startLocation.y < 30) {
                                // ContentViewの現在のオフセット = 初期状態からy軸方向にDragされた距離
                                self.offset = self.closeOffset + value.translation.height
                            }
                        }
                        // 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.offset = self.openOffset
                                }
                            } else {
                                // PageUpViewを閉状態にする
                                self.offset = self.closeOffset
                            }
                        }
            )
        }
    }
}

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

```swift:PageUpView.swift
import SwiftUI

struct PageUpView: View {
    var body: some View {
        Text("This is Up Side.")
    }
}

struct PageUpView_Previews: PreviewProvider {
    static var previews: some View {
        PageUpView()
    }
}
```

# 学習結果
## GeometryReader
自身のViewサイズと座標空間を返す関数をクロージャとして保持するView。
GeometryReaderを通じて自身のView(`.local`)やRootView(`.global`)のサイズ、座標位置を取得することができる。

## onAppear修飾子
Viewが初めて描画されるタイミングで呼び出されるメソッド。

## onDisappear修飾子
Viewが非表示になるタイミングで呼び出されるメソッド。

## Gestureの種類
- TapGesture
 → タップ
- LongPressGesture
 → 長押し
- DragGesture
 → ドラッグ
- MagnificationGesture
 → ピンチ
- RotationGesture
 → 回転

## gesture修飾子
Viewに対してジェスチャーが入力されたときの呼び出しメソッドを実装する。

## GestureのCallback処理
- updating
 → Gestureの値が更新された際に発火(UIの一時的な状態変更)
- onChanged
 → Gestureの値が更新された際に発火(UIの恒久的な状態変更)
- onEnded
 → Gestureが終了した際に発火