[SwiftUI入門] 4.NavigationStackでページの遷移をしよう！

# はじめに
この記事は[[SwiftUI入門] 3.タスクキルしても情報を保存するようにしよう！](https://qiita.com/rinngo0302/items/22f533051c8694b14648)の続きになります。こちらを読んでない人は先にこちらを読むことをおすすめします。

それでは、今回もやっていきましょー

# つくるもの
今回つくるものは前回までつくってきたContentViewは使わず新しいContentViewを使います。

UIの配置はこんな感じです！

![スクリーンショット 2025-04-09 18.01.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/018e23e4-e536-4eec-9e6d-490ce2bea258.png)

そして、「りんご」か「Apple」を押したときは

![スクリーンショット 2025-04-09 18.02.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/bc320e3f-7faa-4163-acbf-9abc734afb16.png)

りんごの画像が出てきて、
「ぶどう」か「Grape」を押したときは

![スクリーンショット 2025-04-09 18.02.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/be3ecf7f-f2e2-48c9-992e-2a6decf33a55.png)

ぶどうの画像が出てきます。

つまり、今回は画面遷移をやっていきましょう！
**これですごくスマホのアプリっぽくなってきます！**

https://github.com/rinngo0302/rinngo_practice/tree/task4

# TODO
- [ ] ContentViewを新規作成＆設定の変更
- [ ] UIの設置
- [ ] 素材の用意
- [ ] 新しいViewの作成
- [ ] NavigationStackとNavigationLinkの適応
- [ ] データを構造体で管理する
- [ ] `.navigationDestination`を使う

# 開発
それでは今回もTODOに沿って開発をしていきましょう！

## ContentViewを新規作成＆設定の変更
今まで使っていたContentViewは置いておいて、新しいContentViewを作成しましょう。
`ContentView.swift`と同じ階層で`ContentView2.swift`を作成してください。そのときテンプレートは「SwiftUI View」を指定してください。

これでTextで「Hello, world!」と書かれたViewが生成されていると思います。

そして、「プロジェクト名App.swift」というファイルがあると思うのですが、その中を見てみましょう。
```Swift
import SwiftUI

@main
struct rinngo_practiceApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```
このようになっていると思いますので、この`ContentView`を先ほど作成した`ContentView2`に変更してください。
```Swift
import SwiftUI

@main
struct rinngo_practiceApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView2()
        }
    }
}
```
この状態で実行してみてください。すると、新しいContentView2の方が表示されると思います。

## UIの設置
![スクリーンショット 2025-04-13 1.30.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/f5d74b24-cdab-49c5-97e2-aaea14d19ba7.png)

上の画像のようにUIを設置していきます。

これは`List`と`Section`を使って作成します。

ListとSectionは下のように使うことができます。
```Swift
List {
    Section(header: Text("タイトル1")) {
        Text("Text1")
        Text("Text2")
    }
    Section(header: Text("タイトル2")) {
        Text("Text1")
        Text("Text2")
    }
}
```
こんな感じで、`List`の中に`Section`を入れ、その`Section`の中にTextを入れることといいです。

それでは、ソースコードはこうなりますのでこれを参考にしてみてください。
```Swift
import SwiftUI

struct ContentView2: View {
    var body: some View {
        List {
            Section(header: Text("日本語")) {
                Text("りんご")
                Text("ぶどう")
            }
            Section(header: Text("English")) {
                Text("Apple")
                Text("Grape")
            }
        }
    }
}

#Preview {
    ContentView2()
}
```

https://developer.apple.com/documentation/swiftui/list

https://developer.apple.com/documentation/swiftui/section

## 素材の用意
次に、「Preview Content」の中の「Preview Assets.xcassets」を開いてください。
![スクリーンショット 2025-04-13 12.38.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/8e4b5983-45ae-412c-9990-578e0806d3b6.png)

このような画面になると思います。

![スクリーンショット 2025-04-13 12.39.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/79732a96-8aa3-44ab-96c5-626d7b325dac.png)

それでは適当にりんごの画像でも持ってきましょう。[いらすとや](https://www.irasutoya.com/)なんかでもいいと思います。
![スクリーンショット 2025-04-13 12.40.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/0f7bf447-c23a-4cbd-8be3-a67720708e3e.png)

それではダウンロードしてきて、

![スクリーンショット 2025-04-13 12.47.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/efa2e411-be21-43fa-8fc0-5de6154e7a95.png)

上の画像の矢印の部分にドラッグ＆ドロップしてください。そして、名前を「Apple」にしておきましょうか。

次に、ぶどうの画像を持ってきて、同じようにGrapeにしましょう。

![スクリーンショット 2025-04-13 12.50.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/9472fea2-ef48-4e07-b062-85248c0f6034.png)

こうなると思います。

次は、先ほどと同じように「New Image Set」で「Grape」を作成して、ぶどうの画像を持ってきて、同じように貼り付けてください。

これで素材の用意ができました。

## 新しいViewの作成
まずは新しいフォルダ「ImageView」を作成して、その中に新しいファイルを「SwiftUI View」のテンプレートを使用してつくりましょう。
名前は「AppleImageView」と、「GrapeImageView」にしてください。

まずは、AppleImageViewを開いてください。

ここには画像だけを表示します。
画像の表示には、`Image`を使います。

https://developer.apple.com/documentation/swiftui/image

```Swift
Image("Image")
```
このように使います。

このImageの引数の中には「Preview Assets.xcassets」で先ほど生成した画像の名前を指定するとよいです。

よって

```Swift
Image("Apple")
```
にすればよいので
```Swift
import SwiftUI

struct AppleImageView: View {
    var body: some View {
        Image("Apple")
    }
}

#Preview {
    AppleImageView()
}
```
![スクリーンショット 2025-04-13 13.15.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/8eab40c5-640e-4bbe-839c-8f883e6f899f.png)

こんな感じになります。ちょっと見切れてますね。まあいっか...

同じように、GrapeImageViewにも同じようにしましょう。

```Swift
import SwiftUI

struct GrapeImageView: View {
    var body: some View {
        Image("Grape")
    }
}

#Preview {
    GrapeImageView()
}
```

![スクリーンショット 2025-04-13 13.21.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/e7599cb6-99c5-47d9-bb20-f05c3da782d1.png)

こんな感じですね。ぎりぎり見切れませんでしたね。

これで遷移先のViewを作成することができました！

## NavigationStackとNavigationLinkの適応
次は画面遷移をつくっていきましょう！

画面遷移には、`NavigationStack`と`NavigationLink`を使います。

|NavigationStack|NavigationLink|
|----|----|
|画面遷移の土台|画面遷移のボタン|

本当にざっくりとした説明ですが、この2つはこのような働きがあります。

https://developer.apple.com/documentation/swiftui/navigationstack

https://developer.apple.com/documentation/swiftui/navigationlink

使い方は
```Swift
NavigationStack {
    NavigationLink(destination: ContentView()) {
        Text("Link")
    }
}
```
このように、`NavigationLink`を`NaviationStack`で囲ってやることで使うことができます。
NavigationStackが画面遷移をする土台のようなもので、このViewの中で画面遷移します。
画面遷移する先のViewは`NavigationLink`の`destination`で指定することができます。
試しに、下のようなソースコードを書いてみましょう。
```Swift
import SwiftUI

struct ContentView2: View {
    var body: some View {
        List {
            Section(header: Text("日本語")) {
                Text("りんご")
                Text("ぶどう")
            }
            Section(header: Text("English")) {
                Text("Apple")
                Text("Grape")
            }
        }
        
        NavigationStack {
            NavigationLink(destination: ContentView()) {
                Text("Link")
            }
        }
    }
}

#Preview {
    ContentView2()
}
```
そうすると、画面の下の方にLinkというボタンが生成され、それをタップするとContentViewでつくったUIが表示されます。

![スクリーンショット 2025-04-13 11.37.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/c957dd1c-c97d-4e34-af38-237798e344ff.png)

タップすると

![スクリーンショット 2025-04-13 11.37.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2735967/0ba7d83c-56ca-45ff-9186-b0486ae04ef9.png)

また、この`NavigationStack`には別の使い方ができるのですが、今はまだできないので後でその機能を試してみましょう。

それでは、`NavigationStack`と`NavigationLink`を設置していきます。

それでは先ほど書いた`NavigationStack`と`NavigationLink`を削除してください。
```Swift
import SwiftUI

struct ContentView2: View {
    var body: some View {
        List {
            Section(header: Text("日本語")) {
                Text("りんご")
                Text("ぶどう")
            }
            Section(header: Text("English")) {
                Text("Apple")
                Text("Grape")
            }
        }
    }
}

#Preview {
    ContentView2()
}
```
こうですね。

では、ここに`NavigationStack`と`NavigationLink`を設定していくのですが、基本的に`NavigationStack`は`List`を囲うように設置し、`NavigationList`はボタンの役割をするので`Text`を囲うようにしてください。

`NavigationStack`はこんな感じで大丈夫です。
```Swift
import SwiftUI

struct ContentView2: View {
    var body: some View {
        NavigationStack {
            List {
                Section(header: Text("日本語")) {
                    Text("りんご")
                    Text("ぶどう")
                }
                Section(header: Text("English")) {
                    Text("Apple")
                    Text("Grape")
                }
            }
        }
    }
}

#Preview {
    ContentView2()
}
```
次に`NavigationLink`です。
```Swift
import SwiftUI

struct ContentView2: View {
    var body: some View {
        NavigationStack {
            List {
                Section(header: Text("日本語")) {
                    NavigationLink(destination: AppleImageView()) {
                        Text("りんご")
                    }
                    NavigationLink(destination: GrapeImageView()) {
                        Text("ぶどう")
                    }
                }
                Section(header: Text("English")) {
                    NavigationLink(destination: AppleImageView()) {
                        Text("Apple")
                    }
                    NavigationLink(destination: GrapeImageView()) {
                        Text("Grape")
                    }
                }
            }
        }
    }
}

#Preview {
    ContentView2()
}
```
こんな感じで、ボタンにしたいところに全部つけていきます。

というわけで、これで一旦は実装し終えましたね！
ですが、ちょっとこのソースコードって...無駄が多い気がしませんか？
`Session`や`Text`、`NavigationLink`など何回も書かなければいけなく、効率が悪そうです。
実際、ここに新たにデータをつけようとするとどんどんソースコードが肥大化していきます。

ということで、次はその解決策について考えていきましょう。

## データを構造体で管理する
データが増えるとその分コードが肥大化し、さらに作業量も増える...という問題点を解決するにはどうすればいいでしょうか？

それは、**データを管理する構造体をつくり、それを一気に読み込んでUIとして反映させる**というものです！

ではつくっていきましょうか。

まずは、データの構造体をつくっていきましょう。

先に、`NavigationLink`ごとの構造体をつくりましょう。
格納するデータは
```Markdown
Item
- Textの文字
- なんの果物か
```
になります。

この構造体をつくっていきましょう。
```Swift
struct Item {
    var text: String
    var fruit: String
}
```
では、次に`Section`ごとの構造体をつくりましょう。
`Section`が持つデータは
```Markdown
ItemSection
- 言語
- Item
```
これを構造体にしてみましょうか。

```Swift
struct SectionItem {
    var lang: String
    var items: [Item]
}
```

こんなもんですかね。

では、次にこの構造体のデータをつくりましょう。
```Swift
var sectionItems = [
    SectionItem(lang: "日本語", items: [
        Item(text: "りんご", fruit: "Apple"),
        Item(text: "ぶどう", fruit: "Grape")
    ]),
    SectionItem(lang: "English", items: [
        Item(text: "Apple", fruit: "Apple"),
        Item(text: "Grape", fruit: "Grape")
    ])
]
```
こんなもんでいいですかね。
これで表示するデータを全て格納することができました。

とりあえずこれを書き込んでいきましょう。
```Swift
import SwiftUI

struct Item {
    var text: String
    var fruit: String
}
struct SectionItem {
    var lang: String
    var items: [Item]
}

struct ContentView2: View {
    var sectionItems = [
        SectionItem(lang: "日本語", items: [
            Item(text: "りんご", fruit: "Apple"),
            Item(text: "ぶどう", fruit: "Grape")
        ]),
        SectionItem(lang: "English", items: [
            Item(text: "Apple", fruit: "Apple"),
            Item(text: "Grape", fruit: "Grape")
        ])
    ]

    var body: some View {
        NavigationStack {
            List {
                Section(header: Text("日本語")) {
                    NavigationLink(destination: AppleImageView()) {
                        Text("りんご")
                    }
                    NavigationLink(destination: GrapeImageView()) {
                        Text("ぶどう")
                    }
                }
                Section(header: Text("English")) {
                    NavigationLink(destination: AppleImageView()) {
                        Text("Apple")
                    }
                    NavigationLink(destination: GrapeImageView()) {
                        Text("Grape")
                    }
                }
            }
        }
    }
}

#Preview {
    ContentView2()
}
```
それでは、本格的にデータを読み込むロジックを組みましょうか。

`Item`も`SectionItem`も構造体の配列なので、`ForEach`を使うとよさそうですね。

まず1つずつ書いていきましょう。`Section`ごとの`ForEach`を書いていきます。
```Swift
NavigationStack {
    List {
        ForEach(sectionItems) { sectionItem in
            Section(header: Text(sectionItem.lang)) {
            }
        }
    }
}
```
これで`Section`ごとの`ForEach`が書けたので、次は`Section`の中を書きましょう。
これも構造体の配列なので、`ForEach`で大丈夫ですね。
```Swift
NavigationStack {
    List {
        ForEach(sectionItems) { sectionItem in
            Section(header: Text(sectionItem.lang)) {
                ForEach(sectionItem.items) { item in
                    if item.fruit == "Apple" {
                        NavigationLink(destination: AppleImageView()) {
                            Text(item.text)
                        }
                    } else {
                        NavigationLink(destination: GrapeImageView()) {
                            Text(item.text)
                        }
                    }
                }
            }
        }
    }
}
```
こんな感じですね。
```Swift
item.fruit == "Apple" {
    NavigationLink(destination: AppleImageView()) {
        Text(item.text)
    }
} else {
    NavigationLink(destination: GrapeImageView()) {
        Text(item.text)
    }
}
```
ここ少し気になりますよね。`NavigationLink`を2回も書いているので、ここも簡単にできそうですよね。
そこで、
```Swift
let view: View = (item.fruit == "Apple") ? AppleImageView() : GrapeImageView()
NavigationLink(destination: view) {
    Text(item.text)
}
```
にしたら簡単に書けそうですが、
``` 
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
```
このようなエラーが出ます。
これは、型推論に時間がかかりすぎて起こるエラーです。

このエラーの原因は
```Swift
let view: View = (item.fruit == "Apple") ? AppleImageView() : GrapeImageView()
```
でViewを生成するのですが、ここで`view`には`AppleImageView`型が入るのか`GrapeImageView`型が入るのかの認識が複雑になってしまうかららしいです。
    たしかに`view`には`View`プロトコルに準拠した型が入るということは分かりますが、それが`AppleImageView`なのか`GrapeImageView`なのかはわからないですよね。(たぶんそういうこと)

この解決策として、変数`view`の型推論がうまくいかないのだから、`view`などの変数を使わないようにすればいいので、上のようなコードにすればうまくいきます。

現状で完成しているソースコードも書いておきますね。
```Swift
import SwiftUI

struct Item: Identifiable {
    var id = UUID()
    var text: String
    var fruit: String
}

struct SectionItem: Identifiable {
    var id = UUID()
    var lang: String
    var items: [Item]
}

struct ContentView2: View {
    var sectionItems = [
        SectionItem(lang: "日本語", items: [
            Item(text: "りんご", fruit: "Apple"),
            Item(text: "ぶどう", fruit: "Grape")
        ]),
        SectionItem(lang: "English", items: [
            Item(text: "Apple", fruit: "Apple"),
            Item(text: "Grape", fruit: "Grape")
        ])
    ]

    var body: some View {
        NavigationStack {
            List {
                ForEach(sectionItems) { sectionItem in
                    Section(header: Text(sectionItem.lang)) {
                        ForEach(sectionItem.items) { item in
                            if item.fruit == "Apple" {
                                NavigationLink(destination: AppleImageView()) {
                                    Text(item.text)
                                }
                            } else {
                                NavigationLink(destination: GrapeImageView()) {
                                    Text(item.text)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

#Preview {
    ContentView2()
}
```

## `.navigationDestination`を使う
あとちょっとです！！がんばりましょう！！
さっきのコードでもいいのですが、`NavigationLink`が2回も出ていて無駄な感じがしますよね。
また、もしこのまま果物の種類が増えるとif文の分岐も増えていきます。

そこで、`.navigationDestination`というものを使いましょう。

まず、なぜ先ほどのコードでは`NavigationLink`が2回も出ていたのかというと
**「定義していたViewが2つもあり、条件によってその2つのViewを使い分けなければいけなかったから」**
ですよね。

つまり、あらかじめ`View`を定義するのではなく、データに合わせて`View`を定義できるようにすればいいのではないでしょうか。

これを実現するのが、`.navigationDestination`になるます。

ではこれの使い方を解説していきます。

まず、`NavigationLink`は
```Swift
NavigationLink(destination: AppleImageView())
```
のように使っていましたが、これを
```Swift
NavigationLink(value: item.fruit)
```
にしましょう。

この機能は先ほども説明したとおり**データに合わせて`View`を定義する**ものですので、どのような`View`にするのかに使うパラメータを渡しましょう。

今回は、果物の種類を表す`item.fruit`でいいですね。

次に、`.navigationDestination`の使い方です。
これは、遷移した後の画面の`View`を定義することができる**トレイリングクロージャ**です。その際、引数として先ほどのパラメータも使うことができます。

また、引数として、そのパラメータの`型`を渡す必要があります。この場合、`item.fruit`は`String`型なので、`String.self`にしてください。これは、`String`型を値として使うものです。`String`だけではだめです。
```Swift
.navigationDestination(for: String.self) { image in
    Image(image)
}
```
これで遷移先の画面の`View`をつくることができました。
それではこれを実装しましょう。

`NavigationStack`に対応させます。
```Swift
import SwiftUI

struct Item: Identifiable {
    var id = UUID()
    var text: String
    var fruit: String
}

struct SectionItem: Identifiable {
    var id = UUID()
    var lang: String
    var items: [Item]
}

struct ContentView2: View {
    var sectionItems = [
        SectionItem(lang: "日本語", items: [
            Item(text: "りんご", fruit: "Apple"),
            Item(text: "ぶどう", fruit: "Grape")
        ]),
        SectionItem(lang: "English", items: [
            Item(text: "Apple", fruit: "Apple"),
            Item(text: "Grape", fruit: "Grape")
        ])
    ]
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(sectionItems) { sectionItem in
                    Section(header: Text(sectionItem.lang)) {
                        ForEach(sectionItem.items) { item in
                            NavigationLink(value: item.fruit) {
                                    Text(item.text)
                            }
                        }
                    }
                }
            }
            .navigationDestination(for: String.self) {image in
                Image(image)
            }
        }
    }
}

#Preview {
    ContentView2()
}
```
こんな実装で大丈夫です！

これで、ようやく完成です！:v:

https://developer.apple.com/documentation/swiftui/navigationstack

https://developer.apple.com/documentation/swiftui/navigationlink

# さいごに
今回もお疲れ様でしたー！

本当に長くて大変でしたね...
スマホ特有の機能を扱う章で、かつ私自身が少し詰まってしまったところだったので結構詳しく書いてみました。
(ですけど...最後のほうはちょっと適当になっちゃったかも...)

もしかしたら分かりづらいところもあるかもしれないので、そのときはもっと勉強してみてください！

それでは次もがんばりましょー！

次回
前回: [[SwiftUI入門] 3.タスクキルしても情報を保存するようにしよう！](https://qiita.com/rinngo0302/items/22f533051c8694b14648)