NavigationStackを一番活かす人（SwiftUI）

こんにちは。[フエルマネー](https://apps.apple.com/us/app/%E3%83%95%E3%82%A8%E3%83%AB%E3%83%9E%E3%83%8D%E3%83%BC/id6446435231)開発者です。

この度コードを１から書き直すという暴挙に出た次第ですが、[前回](https://qiita.com/yoshi-eng/items/52f7b9aa1d8d7826bdba)に引き続いて次に行うことはズバリ、画面遷移設計ですね。

# NavigationStackとは
iOS16時点で推奨されている、画面遷移のためのViewです。
以前のNavigationViewを使っていて、なかなか移行に腰が上がらない人もいるでしょう。僕も当初はNavigationViewの機能だけで済ませようと思っていました。

でもここでついていかないと、時代に取り残されて未だにObjective-Cで開発している人の二の舞になると思ったので移行しました。

意外と楽しいので、ここで使いこなせるようになっておきましょう。

# 何ができるの？
NavigationStackは、pathに格納した情報をもとに、画面遷移状態すなわち現在地を把握することができます。
```swift:ContentVIew.swift
struct ContentView: View {
    @State var path = NavigationPath()
    
    var body: some View{
        NavigationStack(path: $path){
            Text("hoge")
                .navigationTitle("ホーム")
                .navigationBarTitleDisplayMode(.inline)
        }
    }
}
```
とりあえず、土台はこんな感じです。

.navigationBarTitleDisplayMode(.inline)は僕の好みなので、なくてもいいです。何が違うかは実際に変えて試してみてください。Navigationタイトルの見た目が変わるだけです。

NavigationStackの引数pathにBindingした値が、画面遷移状態を把握するためのStateとなります。このpathの値は、条件を満たすコレクションならなんでも（Int配列、String配列などでも）良いらしいですが、

```swift
@State var path = NavigationPath()
```
NavigationPathというコレクションはこれのためだけ（？）に生まれたなんでも入る配列です。
結論、これを使えば良いという話です。

# 基本的な画面遷移
まずは以下をご覧ください。
```swift: ContentView.swift
struct ContentView: View {
    @State var path = NavigationPath()
    
    var body: some View{
        NavigationStack(path: $path){
            Button("遷移する"){
                path.append(0)
            }
            .navigationTitle("ホーム")
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(for: Int.self, destination: { _ in
                Text("hoge")
            })
        }
    }
}
```
**結果**
<img width="250" alt="1-2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3518353/e756585a-3d8a-0e49-2987-3363f01570bc.gif">
以下のモディファイアにて、pathにIntがappendされた時に遷移するDestination(=View)を定義します。
```swift
.navigationDestination(for: Int.self, destination: { _ in
    Text("hoge")
})
```
Intであればなんでも反応します。つまりIntの中身によって遷移先の情報を変えられます。
```swift:ContentView.swift
struct ContentView: View {
    @State var path = NavigationPath()
    
    var body: some View{
        NavigationStack(path: $path){
            VStack{
                Button("hoge0に遷移する"){
                    path.append(0)
                }
                Button("hoge1に遷移する"){
                    path.append(1)
                }
                Button("hoge2に遷移する"){
                    path.append(2)
                }
                Button("hoge3に遷移する"){
                    path.append(3)
                }
                Button("hoge4に遷移する"){
                    path.append(4)
                }
            }
            .navigationTitle("ホーム")
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(for: Int.self, destination: { appended in
                Text("hoge\(appended)")
            })
        }
    }
}
```
これはIntでなくても、Stringでも良いですし、オリジナルのクラスでも構いません。
ただし注意して欲しいのは、遷移先(子画面としましょう)で同様にIntをappendすると、親画面で定義した上記のNavigationDestinationが呼ばれます。複数定義するとエラーの元となるので好き勝手につけるのは危険です。
他のViewと絶対に干渉したくない方は、このView内に専用の列挙体を作ることをお勧めします。
```diff_swift:ContentView.swift
struct ContentView: View {
    @State var path = NavigationPath()
    
+    enum InnerPath: Int{
+        case hoge0, hoge1, hoge2, hoge3, hoge4
+    }
    
    var body: some View{
        NavigationStack(path: $path){
            VStack{
                Button("hoge0に遷移する"){
+                    path.append(InnerPath.hoge0)
                }
                Button("hoge1に遷移する"){
+                    path.append(InnerPath.hoge1)
                }
                Button("hoge2に遷移する"){
+                    path.append(InnerPath.hoge2)
                }
                Button("hoge3に遷移する"){
+                    path.append(InnerPath.hoge3)
                }
                Button("hoge4に遷移する"){
+                    path.append(InnerPath.hoge4)
                }
            }
            .navigationTitle("ホーム")
            .navigationBarTitleDisplayMode(.inline)
+            .navigationDestination(for: InnerPath.self, destination: { appended in
+                Text("hoge\(appended.rawValue)")
+            })
        }
    }
}
```

# NavigationLinkを用いた遷移
NavigationLinkは以前からありますが、馴染みのない引数「value」があるかと思います。これが先ほどpathにappendしてた値に対応します。
![1-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3518353/74852170-7732-4d81-202d-8a5beb998a8c.png)
つまり先ほどの基本的な遷移では、コードベースでいつでもpathにappendすれば遷移できましたが、簡単な遷移であればNavigationLinkで事足ります。タップしたらvalueに設定した値がpathにappendされます。
```swift:ContentView.swift
struct ContentView: View {
    @State var path = NavigationPath()
    
    enum InnerPath: Int{
        case hoge0, hoge1, hoge2, hoge3, hoge4
    }
    
    var body: some View{
        NavigationStack(path: $path){
            VStack{
                NavigationLink("hoge0に遷移する", value: InnerPath.hoge0)
                NavigationLink("hoge1に遷移する", value: InnerPath.hoge1)
                NavigationLink("hoge2に遷移する", value: InnerPath.hoge2)
                NavigationLink("hoge3に遷移する", value: InnerPath.hoge3)
                NavigationLink("hoge4に遷移する", value: InnerPath.hoge4)
            }
            .navigationTitle("ホーム")
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(for: InnerPath.self, destination: { appended in
                Text("hoge\(appended.rawValue)")
            })
        }
    }
}
```
一気にスッキリしましたね。
このようにNavigationStackを使うと、似たような内容の遷移を一挙に書けるのでコードがコンパクトになります。Destinationを一個ずつ書く必要はありません。

# 上級者編
さて、ここからが本題です。ここまでは基本的なNavigationStackの説明でしたが、ここからは応用編で、さらにコードを簡潔にするアイデアを共有します。

まずは以下をご覧ください。
```swift:HomePath
enum HomePath: Int{
    case root, setting, account, variable, fixed, premium, explain, information
    
    var toString: String{
        ["ホーム", "設定", "資産を管理", "変動収支を管理", "固定収支を管理", "プレミアム会員になる", "操作説明", "お知らせ"][self.rawValue]
    }
    
    @ViewBuilder
    func Destination() -> some View{
        switch self {
        case .root: HomeView()
        case .setting: SettingView()
        case .account: AccountView()
        case .variable: VariableView()
        case .fixed: FixedView()
        case .premium: PremiumView()
        case .explain: ExplainView()
        case .information: InformationView()
        }
    }
}
```
これは、フエルマネーのホーム画面以下の画面遷移設計です。

ホーム画面(ContentView)からは設定画面(SettingView)、操作説明画面(ExplainView)、お知らせ画面(InformationView)の3つに遷移できます。

設定画面(SettingView)からは、資産管理(AccountView)、変動収支管理(VariableView)、固定収支管理(FixedView)、プレミアム登録画面(PremiumView)に遷移できます。

実際の画面遷移は階層構造になっているのですが、この列挙体ではそれをガン無視して全画面を列挙しています。

これの何が良いかというと、、、

```swift:ContentView.swift
struct ContentView: View {
    @State var path = NavigationPath()
    
    var body: some View{
        NavigationStack(path: $path){
            List{
                NavigationLink(HomePath.setting.toString, value: HomePath.setting)
                NavigationLink(HomePath.explain.toString, value: HomePath.explain)
                NavigationLink(HomePath.information.toString, value: HomePath.information)
            }
            .navigationTitle(HomePath.root.toString)
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(for: HomePath.self, destination: { appended in
                appended.Destination()
                    .navigationTitle(appended.toString)
                    .navigationBarTitleDisplayMode(.inline)
            })
        }
    }
}
```
子階層含め全ての画面遷移がルートのホーム画面で定義できます。しかもNavigationTitleまで対応しています。
なぜかというと、遷移先の子画面で同じクラスのpath(ここでいうHomePath)を入れても、親画面の.navigationDestinationが呼ばれるからです。

SettingViewでもさらに孫階層へ画面遷移をするわけですが、その処理はHomeView上で完結しています。なのでSettingViewでやることといえば

```swift:SettingView.swift
struct SettingView: View{
    var body: some View{
        List{
            NavigationLink(HomePath.account.toString, value: HomePath.account)
            NavigationLink(HomePath.variable.toString, value: HomePath.variable)
            NavigationLink(HomePath.fixed.toString, value: HomePath.fixed)
            NavigationLink(HomePath.premium.toString, value: HomePath.premium)
        }
    }
}
```
たったのこれだけなんです。

他の画面も適当に（とりあえずTextだけで）作って、実行してみましょう。

**結果**
<img width="250" alt="1-3.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3518353/1a545b33-8c9d-811f-ef76-736ea2ac1bb6.gif">

これだけ豊富な遷移画面を作っているのに、なんとコード行数は100行を切ります。

皆さんもSwiftUIを使いこなしてスタイリッシュな開発を楽しんでください！

一応、全体のソースを貼っときます。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @State var path = NavigationPath()
    
    var body: some View{
        NavigationStack(path: $path){
            List{
                NavigationLink(HomePath.setting.toString, value: HomePath.setting)
                NavigationLink(HomePath.explain.toString, value: HomePath.explain)
                NavigationLink(HomePath.information.toString, value: HomePath.information)
            }
            .navigationTitle(HomePath.root.toString)
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(for: HomePath.self, destination: { appended in
                appended.Destination()
                    .navigationTitle(appended.toString)
                    .navigationBarTitleDisplayMode(.inline)
            })
        }
    }
}

struct SettingView: View{
    var body: some View{
        List{
            NavigationLink(HomePath.account.toString, value: HomePath.account)
            NavigationLink(HomePath.variable.toString, value: HomePath.variable)
            NavigationLink(HomePath.fixed.toString, value: HomePath.fixed)
            NavigationLink(HomePath.premium.toString, value: HomePath.premium)
        }
    }
}

enum HomePath: Int{
    case root, setting, account, variable, fixed, premium, explain, information
    
    var toString: String{
        ["ホーム", "設定", "資産を管理", "変動収支を管理", "固定収支を管理", "プレミアム会員になる", "操作説明", "お知らせ"][self.rawValue]
    }
    
    @ViewBuilder
    func Destination() -> some View{
        switch self {
        case .root: ContentView()
        case .setting: SettingView()
        case .account: AccountView()
        case .variable: VariableView()
        case .fixed: FixedView()
        case .premium: PremiumView()
        case .explain: ExplainView()
        case .information: InformationView()
        }
    }
}

struct AccountView: View{
    var body: some View{
        Text("AccountView")
    }
}

struct VariableView: View{
    var body: some View{
        Text("VariableView")
    }
}

struct FixedView: View{
    var body: some View{
        Text("FixedView")
    }
}

struct PremiumView: View{
    var body: some View{
        Text("PremiumView")
    }
}

struct ExplainView: View{
    var body: some View{
        Text("ExplainView")
    }
}

struct InformationView: View{
    var body: some View{
        Text("InformationView")
    }
}
```