0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

この記事は[SwiftUI入門] 3.タスクキルしても情報を保存するようにしよう!の続きになります。こちらを読んでない人は先にこちらを読むことをおすすめします。

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

つくるもの

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

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

スクリーンショット 2025-04-09 18.01.23.png

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

スクリーンショット 2025-04-09 18.02.08.png

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

スクリーンショット 2025-04-09 18.02.55.png

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

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

TODO

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

開発

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

ContentViewを新規作成&設定の変更

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

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

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

import SwiftUI

@main
struct rinngo_practiceApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

このようになっていると思いますので、このContentViewを先ほど作成したContentView2に変更してください。

import SwiftUI

@main
struct rinngo_practiceApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView2()
        }
    }
}

この状態で実行してみてください。すると、新しいContentView2の方が表示されると思います。

UIの設置

スクリーンショット 2025-04-13 1.30.35.png

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

これはListSectionを使って作成します。

ListとSectionは下のように使うことができます。

List {
    Section(header: Text("タイトル1")) {
        Text("Text1")
        Text("Text2")
    }
    Section(header: Text("タイトル2")) {
        Text("Text1")
        Text("Text2")
    }
}

こんな感じで、Listの中にSectionを入れ、そのSectionの中にTextを入れることといいです。

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

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()
}

素材の用意

次に、「Preview Content」の中の「Preview Assets.xcassets」を開いてください。
スクリーンショット 2025-04-13 12.38.00.png

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

スクリーンショット 2025-04-13 12.39.00.png

それでは適当にりんごの画像でも持ってきましょう。いらすとやなんかでもいいと思います。
スクリーンショット 2025-04-13 12.40.50.png

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

スクリーンショット 2025-04-13 12.47.38.png

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

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

スクリーンショット 2025-04-13 12.50.50.png

こうなると思います。

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

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

新しいViewの作成

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

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

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

Image("Image")

このように使います。

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

よって

Image("Apple")

にすればよいので

import SwiftUI

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

#Preview {
    AppleImageView()
}

スクリーンショット 2025-04-13 13.15.46.png

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

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

import SwiftUI

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

#Preview {
    GrapeImageView()
}

スクリーンショット 2025-04-13 13.21.46.png

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

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

NavigationStackとNavigationLinkの適応

次は画面遷移をつくっていきましょう!

画面遷移には、NavigationStackNavigationLinkを使います。

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

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

使い方は

NavigationStack {
    NavigationLink(destination: ContentView()) {
        Text("Link")
    }
}

このように、NavigationLinkNaviationStackで囲ってやることで使うことができます。
NavigationStackが画面遷移をする土台のようなもので、このViewの中で画面遷移します。
画面遷移する先のViewはNavigationLinkdestinationで指定することができます。
試しに、下のようなソースコードを書いてみましょう。

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

タップすると

スクリーンショット 2025-04-13 11.37.38.png

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

それでは、NavigationStackNavigationLinkを設置していきます。

それでは先ほど書いたNavigationStackNavigationLinkを削除してください。

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()
}

こうですね。

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

NavigationStackはこんな感じで大丈夫です。

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です。

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()
}

こんな感じで、ボタンにしたいところに全部つけていきます。

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

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

データを構造体で管理する

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

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

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

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

先に、NavigationLinkごとの構造体をつくりましょう。
格納するデータは

Item
- Textの文字
- なんの果物か

になります。

この構造体をつくっていきましょう。

struct Item {
    var text: String
    var fruit: String
}

では、次にSectionごとの構造体をつくりましょう。
Sectionが持つデータは

ItemSection
- 言語
- Item

これを構造体にしてみましょうか。

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

こんなもんですかね。

では、次にこの構造体のデータをつくりましょう。

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")
    ])
]

こんなもんでいいですかね。
これで表示するデータを全て格納することができました。

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

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()
}

それでは、本格的にデータを読み込むロジックを組みましょうか。

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

まず1つずつ書いていきましょう。SectionごとのForEachを書いていきます。

NavigationStack {
    List {
        ForEach(sectionItems) { sectionItem in
            Section(header: Text(sectionItem.lang)) {
            }
        }
    }
}

これでSectionごとのForEachが書けたので、次はSectionの中を書きましょう。
これも構造体の配列なので、ForEachで大丈夫ですね。

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)
                        }
                    }
                }
            }
        }
    }
}

こんな感じですね。

item.fruit == "Apple" {
    NavigationLink(destination: AppleImageView()) {
        Text(item.text)
    }
} else {
    NavigationLink(destination: GrapeImageView()) {
        Text(item.text)
    }
}

ここ少し気になりますよね。NavigationLinkを2回も書いているので、ここも簡単にできそうですよね。
そこで、

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

このようなエラーが出ます。
これは、型推論に時間がかかりすぎて起こるエラーです。

このエラーの原因は

let view: View = (item.fruit == "Apple") ? AppleImageView() : GrapeImageView()

でViewを生成するのですが、ここでviewにはAppleImageView型が入るのかGrapeImageView型が入るのかの認識が複雑になってしまうかららしいです。
たしかにviewにはViewプロトコルに準拠した型が入るということは分かりますが、それがAppleImageViewなのかGrapeImageViewなのかはわからないですよね。(たぶんそういうこと)

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

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

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

NavigationLink(destination: AppleImageView())

のように使っていましたが、これを

NavigationLink(value: item.fruit)

にしましょう。

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

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

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

また、引数として、そのパラメータのを渡す必要があります。この場合、item.fruitString型なので、String.selfにしてください。これは、String型を値として使うものです。Stringだけではだめです。

.navigationDestination(for: String.self) { image in
    Image(image)
}

これで遷移先の画面のViewをつくることができました。
それではこれを実装しましょう。

NavigationStackに対応させます。

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:

さいごに

今回もお疲れ様でしたー!

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

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

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

次回
前回: [SwiftUI入門] 3.タスクキルしても情報を保存するようにしよう!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?