【SwiftUI】NavigationStackのpathを使って、画面一覧を一挙に宣言して自動遷移させてみた

## NavigationStack
従来の`NavigationView`はiOS16以降で非推奨となり、代わりに`NavigationStack`の導入が推奨されています。

## 基本的な使い方
`NavigationView`と同じように使うことができます。
```swift
struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("次の画面へ") {
                NextView()
            }
        }
    }
}
```

## `NavigationPath`と`navigationDestination`を使った実装
`NavigationPath`を与えることで、pathの変更を監視して画面遷移させることができます。
`["path A", "path B", "path C"]`と配列の要素数に応じて階層は深くなっていき、最後尾の要素が現在の画面に渡されます。

```swift
struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            Button("Pathを追加") {
                path.append("new path")
            }
            .navigationDestination(for: String.self, destination: { _ in
                NextView()
            })
        }
    }
}
```

## 画面一覧を列挙して自動遷移させる

実は`Hashable`に準拠するコレクションであれば、何でも`NavigationPath`として使うことができます。
これを利用して`NavigationStack`の配下に置く画面一覧を`enum`で宣言し、その配列をpathとして渡します。
さらに`@ViewBuilder`関数を使うことによって、対応する`View`まで一挙に宣言してしまいます。
ルートの`navigationDestination`に`screenKey.destination()`を記述しておけば、あとはどこでもpathの配列を操作するだけで、自動的に対応するViewへ遷移させることができます。

```swift
enum ScreenKey {
    case home
    case fruitList
    case fruitDetail
    
    // 対応するViewのインスタンスを返す
    @ViewBuilder 
    func destination() -> some View {
        switch self {
        case .home:
            HomeView()
        case .fruitList:
            FruitListView()
        case .fruitDetail:
            FruitDetailView()
        }
    }
}

struct RootView: View {
    @State private var path = [ScreenKey]()
    
    var body: some View {
        NavigationStack(path: $path) {
            HomeView()
                // ルートに記述するだけでokです
                .navigationDestination(for: ScreenKey.self, destination: { screenKey in 
                    screenKey.destination()
                })
        }
    }
}
```

## `@EnvironmentObject`として各画面でpathを操作する
どの階層でもpathを操作できるようにするために`NavigationStack`の`environmentObject`に設定してしまいます。
これで`NavigationStack`配下全てのViewからpathを操作できるようになりました。

以下の実装は
- ホーム画面
- フルーツ一覧画面
- フルーツ詳細画面

と深い階層へ画面が遷移していく実用的なケースです。
詳細画面には`id`が必要なため`ScreenKey`に値を持たせます。

```swift
// environmentObjectに設定するためObservableObjectに準拠させます
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
}
```
## まとめ
ルートの`navigationDestination`さえ設定してしまえば、以降の階層では配列を操作するだけで、自動的に画面遷移をさせることができました。
`NavigationStack`が表示し得る画面は`NavigationRouter`に明示的に宣言するといった実装で、まさにSwiftUIって感じが大満足です。
`NavigationLink`からの解放も嬉しい。
この実装方法では、ディープリンクや、ログイン状態による分岐、ログアウト処理などを直感的でシンプルに書くこともできるので別で記事にしたいと思います。
お役に立てれば幸いです！

## 追記
> この実装方法では、ディープリンクや、ログイン状態による分岐、ログアウト処理などを直感的でシンプルに書くこともできるので別で記事にしたいと思います。

してみました！

https://qiita.com/fav_n/items/4576b573488bca5556d3