【SwiftUI】NavigationStackのpathを使って、ログアウト遷移やディープリンク遷移を直感的に書いてみた

## 前回のおさらい
`Hashable`に準拠するコレクションとして画面一覧を`enum`で宣言して`NavigationStack`にpathとして渡し、`@ViewBuilder`関数を使うことで対応するViewに自動遷移させることができました。


https://qiita.com/fav_n/items/235085157d2f9a334783

```swift
class NavigationRouter: ObservableObject { 
    @Published var path = [ScreenKey]()

    enum ScreenKey: Hashable {
        case home
        case fruitList
        case fruitDetail(id: String)

        @ViewBuilder
        func destination() -> some View {
            switch self {
            case .home:
                HomeView()
            case .fruitList:
                FruitListView()
            case .fruitDetail(let id):
                FruitDetailView(id: id)
            }
        }
    }
}

struct RootView: View {
    @StateObject private var navigationRouter = NavigationRouter()
    
    var body: some View {
        NavigationStack(path: $navigationRouter.path) {
            HomeView()
                .navigationDestination(for: NavigationRouter.ScreenKey.self, destination: { screenKey in
                    screenKey.destination()
                })
        }
        // NavigationStack自身のmodifierとして記述するよう注意してください
        .environmentObject(navigationRouter)
    }
}

struct HomeView: View {
    @EnvironmentObject private var navigationRouter: NavigationRouter
    
    var body: some View {
        Button("フルーツ一覧画面へ") {
            navigationRouter.path.append(.fruitList)
        }
    }
}

struct FruitListView: View {
    @EnvironmentObject private var navigationRouter: NavigationRouter
    private let fruitList = [Fruit(id: "0", name: "apple"),
                             Fruit(id: "1", name: "orange"),
                             Fruit(id: "2", name: "peach")]
    
    var body: some View {
        List(fruitList) { fruit in
            Button("\(fruit.name)詳細画面へ") {
                navigationRouter.path.append(.fruitDetail(id: fruit.id))
            }
        }
    }
}

struct Fruit {
    let id: String
    let name: String
}
```

## ログアウト処理など、ルート画面へのpopを直感的に書く
ログイン状態によってログイン画面orホーム画面を表示する、などイニシャル画面の分岐がある一方、ログアウト処理では共にログイン画面に戻る、といった遷移要件はよくあるケースだと思います。
以下の実装では、pathと`navigationBarBackButtonHidden()`を活用することによって、イニシャル画面を分岐させつつ、内部的な配列はどのケースでもログイン画面を経由している状態を実現しています。
これによってルートへのpopを簡潔に記述でき、さらに`NavigationStack`標準のpopアニメーションを担保しています。

```diff_swift
struct RootView: View {
    @StateObject private var navigationRouter = NavigationRouter()
    
    var body: some View {
        NavigationStack(path: $navigationRouter.path) {
            LoginView()
                .navigationDestination(for: NavigationRouter.ScreenKey.self, destination: { screenKey in
                    screenKey.destination()
                })
        }
        .environmentObject(navigationRouter)
+       .onAppear {
+           setInitialPath()
+       }
    }

+   private func setInitialPath() {
+       guard isLoggedIn else { return }
+       navigationRouter.path.append(.home)
+   }
}

struct HomeView: View {
    var body: some View {
        Text("ホーム画面")
+           .navigationBarBackButtonHidden()
    }
}

struct LogoutView: View {
    @EnvironmentObject private var navigationRouter: NavigationRouter
    
+   private func logout() {
+       navigationRouter.path.removeAll()
+   }
}
```
## ディープリンクでも階層構造を担保できる
さらにこの実装では、ディープリンクなどの操作を伴わない画面遷移でも、通常操作ケースと同じように画面階層を担保することができます。
例えば、通常操作ではホーム画面→リスト画面→詳細画面と遷移するのに、詳細画面へディープリンクを使って遷移したことで、ホーム画面→詳細画面の階層構造となってしまい、戻り先が通常操作と異なってしまう、といったケースはよくあると思います。
以下の実装では、配列に`.fruitList`を追加することによって、ディープリンクでも画面の階層構造を担保しています。
```diff_swift
struct RootView: View {
    @StateObject private var navigationRouter = NavigationRouter()
    
    var body: some View {
        NavigationStack(path: $navigationRouter.path) {
            HomeView()
                .navigationDestination(for: NavigationRouter.ScreenKey.self, destination: { screenKey in
                    screenKey.destination()
                })
        }
+       .onOpenURL(perform: { url in
+           // ディープリンク判定省略
+           switch deepLink {    
+           case .fruitDetail(let id):
+               // .fruitListも追加する
+               navigationRouter.path = [.fruitList, .fruitDetail(id: id)]
+           }
+       })
    }
}
```
## 感想
実用的なケースだと思うので是非みなさんのお役に立てればと思って書いてみました。
初投稿で拙い部分もあったかと思いますが、最後まで読んでいただきありがとうございました！