SwiftUI で複数の画面を一度に戻る方法

# はじめに
執筆環境：Xcode 12.4 / macOS Big Sur 11.2.3

**ホーム＞決済画面＞決済完了画面**
このように3回以上の画面遷移を行ったとき、一度に「ホーム」に戻る方法を解説いたします。

この記事は **寄付を身近にする dim.** の開発を行う中での学びを共有する目的で執筆しております。
TechCrunch さんにも取り上げていただきましたので御覧ください。

https://jp.techcrunch.com/2021/03/18/dim/

# 概要
- 一気に消さないといけない画面が多い場合
  - Environment を利用する
- 一気に消さないといけない画面が少ない場合
  - dismiss を伝搬させる方法でも良いかも

# 0. 基礎
自分自身を閉じようとする場合は、以下のような処理で実装が可能です。

```swift
struct TransitionSampleView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {
        Button(
            "dismiss",
            action: {
                // TransitionSampleView を閉じる
                presentationMode.wrappedValue.dismiss()
            }
        )
    }
}
```

`sheet` などで画面遷移を実現している場合、 Binding されている isPresented の値を false にすることで画面を閉じることも可能です。

```swift
struct TransitionSampleView: View {
    @State var isNext: Bool = false // 何かしらの方法でこれを切り替える

    var body: some View {
        Text("SAMPLE")
            .sheet(isPresented: $isNext) {
                Text("NEXT")
            }
    }
}
```

このどちらを利用した実装方法であるかを意識しながら読み進めていただくと、より理解が深まるかと思います。

# 1. dismiss 伝搬
`sheet` などの `onDismiss` で自身を閉じる処理を記述する方法です。
**ホーム＞決済画面＞決済完了画面**
このように遷移させて決済完了画面からホームに戻るとき、決済画面の `sheet` に以下を実装するイメージです。

```swift
struct TransitionSampleView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @State var isNext: Bool = false // 何かしらの方法でこれを切り替える

    var body: some View {
        Text("SAMPLE")
            .sheet(
                isPresented: $isNext,
                onDismiss: {
                    // 遷移先が閉じたとき、自身も閉じる
                    presentationMode.wrappedValue.dismiss()
                },
                content: {
                    Text("NEXT")
                }
            )
    }
}
```

メリット：非常にシンプル
デメリット：一瞬で戻るというよりは、1画面ずつ段階的に戻るので、画面数が多いとユーザ体験を悪化させる可能性がある

# 2. Environment
EnvironmentKey を用いた方法です。
**ホーム＞決済画面＞決済完了画面**
このように遷移させて決済完了画面からホームに戻るとき、ホームに環境変数設定、決済完了画面に環境変数を利用した dismiss 処理を記述します。

```swift
// 閉じる・開くの制御なので Bool
struct PresentingModalKey: EnvironmentKey {
    static let defaultValue = Binding<Bool>.constant(false)
}
// 自作環境変数
extension EnvironmentValues {
    var isPresentingModal: Binding<Bool> {
        get { self[PresentingModalKey.self] }
        set { self[PresentingModalKey.self] = newValue }
    }
}
```

まずは、遷移元の実装です。
dismiss 伝搬とほぼ変わりませんが、 **遷移先の画面に対して Environment を設定している** 箇所が追加になっています。逆に `PresentationMode` は **利用していない** ことがわかるかと思います。

```swift
struct ContentView: View {
    @State var isPresentingModal: Bool = false

    var body: some View {
        Button(
            "NEXT",
            action: {
                isPresentingModal = true
            }
        )
        // 環境変数として利用する変数をトリガーとする
        .sheet(isPresented: $isPresentingModal) {
            NavigationView {
                Text("NEXT")
            }
            // 子 View に環境変数を設定
            .environment(\.isPresentingModal, $isPresentingModal)
        }
    }
}
```

最後に、遷移後の実装です。
Environment value の取得方法は SwiftUI で最初から実装されているものを利用するときと同様です。

```swift
struct CompletedView: View {
    // 環境変数の取得
    @Environment (\.isPresentingModal) var isPresentedModally

    var body: some View {
        Button(
            "CLOSE",
            action: {
                // 環境変数に対して状態変更を適応
                isPresentedModally.wrappedValue = false
            }
        )
    }
}
```

メリット：一瞬で複数画面を閉じることができる
デメリット：Environment はアクセス可能領域が自然と広くなるので乱用厳禁、 sheet だけでの遷移の場合正常に動作しない（記事執筆段階）

# おわりに
最後までご覧いただきありがとうございます。
UIKit でも画面遷移周りをどのように管理するかは、非常に重要なポイントなので、今後も引き続き SwiftUI を利用する場合の画面遷移管理方法を模索していきます。