【SwiftUI】NavigationStack (iOS16、iPadOS16以降)

SwiftUI

iOS16が正式にリリースされたのでNavigationStackについて見ていきましょう。

NavigationStackとは

NavigationStackはiOS16以降で使用できるNavigationViewに代わるものです。
これに伴いNavigationViewはDeprecatedになりました。

当然ですがiOS15までは使用出来ませんので、
今すぐに置き換えるなければならない訳ではありませんが、
今後、iOS16以降のみ対応のアプリのリリースやアップデートに備えて確認しておきましょう。

NavigationStackの使い方

NavigationStackを使ってみる

NavigationStackはNavigationViewと殆ど同じ様に使うことが出来ます。
簡単な内容ならばそのまま置き換えるだけでも動作します。

struct BasicNavigationStackView: View {
    var body: some View {
        NavigationStack{
            List{
                NavigationLink("LinkLabelText", destination: Text("Destination"))
                NavigationLink("LinkLabelText2", destination: Text("Destination2"))
            }
        }
    }
}

NavigationStackとNavigationViewの違い

NavigationStackでは画面遷移先をNavigationLinkと分けて書くことが出来ます。
入れ子が減ったりNavigationLink部分が短くなるので、可読性が上がる場合もあると思います。

struct BasicNavigationStackView: View {
    var body: some View {
        NavigationStack{
            List{
                NavigationLink("LinkLabelText", value: "Destination")
                NavigationLink("LinkLabelText2", value: "Destination2")
            }
            .navigationDestination(for: String.self) { text in
                Text(text)
            }
        }
    }
}

NavigationLinkの変化

NavigationLinkのdestinationはvalueに変わりました。
ここに遷移先のViewに関わる値を渡します。

//old
NavigationLink("LinkLabelText", destination: Text("Destination"))

//new
NavigationLink("LinkLabelText", value: "Destination")

今回はdestinationでText Viewを表示していました。
その為valueにはTextに使用するStringを渡します。

valueに任意の構造体やクラスを渡したい場合はHashableを実装する事で使用可能になります。

struct SampleStruct: Hashable {
}

NavigationLink("LinkLabelText", value: SampleStruct())

navigationDestination

遷移先はnavigationDestination Modifierで指定します。

NavigationStack{
    List{
        NavigationLink("LinkLabelText", value: "Destination")
        NavigationLink("LinkLabelText2", value: "Destination2")
    }
    .navigationDestination(for: String.self) { text in
        Text(text)
    }
}

navigationDestinationはNavigationStackより内側に書く必要があります。
今回はListの後に書いています。Listでループなどを行う場合はこの位置が良いでしょう。
NavigationLinkが1つのみであればNavigationLinkでも構いません。

navigationDestination自体はViewプロトコルに実装されている為、
かなり自由な場所に書く事が可能です。

.navigationDestination(for: String.self) { text in
    Text(text)
}

.navigationDestination(for: SampleStruct.self) { sampleStruct in
   SampleView(sampleStruct)
}

navigationDestinationでは引数に先程のvalueで渡した型のselfを渡します。
するとクロージャ側では受け取った値を使用して遷移先のViewを表示する事が出来ます。

遷移先の分岐

navigationDestination内では条件分岐が可能です。
navigationDestinationに渡された値を確認して遷移先を変える事が出来ます。

また、navigationDestinationは複数あっても構いません。
NavigationLinkのvalueの型に合わせて複数用意することでも遷移先を変える事が出来ます。

struct BasicNavigationStackView: View {
    var body: some View {
        NavigationStack{
            List{
                NavigationLink("LinkLabelText", value: "Destination")
                NavigationLink("LinkLabelText2", value: "Destination2")
                NavigationLink("LinkLabelText3", value: 3)
            }
            .navigationDestination(for: String.self) { text in
                if text == "Destination" {
                    Text("Destination")
                }else{
                    Text("Not Destination")
                }
            }
            .navigationDestination(for: Int.self) { value in
                Text("Destination \(value)")
            }
        }
    }
}

navigationDestinationの優先順位

navigationDestinationで同じ型を受け取るものが複数ある場合、
先に読み込まれたものが優先されます。

NavigationStack{
    List{
        NavigationLink("LinkLabelText", value: "Destination")
    }
    .navigationDestination(for: String.self) { text in
        Text("Valid")
    }
    .navigationDestination(for: String.self) { text in
        Text("Invalid")
    }
}

NavigationStack{
    List{
        NavigationLink("LinkLabelText", value: "Destination")
            .navigationDestination(for: String.self) { text in
                Text("Valid")
            }
    }
    .navigationDestination(for: String.self) { text in
        Text("Invalid")
    }
}

通常は上に書いたものが優先されるという意識で良いでしょう。
読みやすさを考えるとなるべくまとめて書いた方が良いと思います。

注意が必要なのは画面遷移が挟まった場合です。
画面遷移が挟まり別なViewとなった場合に優先順位が分かりにくくなります。

struct SampleViewA: View {
    var body: some View {
        NavigationStack {
            List{
                NavigationLink("Go to SampleB",value: "sampleB")
            }
            .navigationDestination(for: String.self) { text in
                SampleViewB()
                Text("Valid")
            }
        }
    }
}

struct SampleViewB: View {
    var body: some View {
        List{
            NavigationLink("Go to SampleC",value: "sampleC")
        }
        .navigationDestination(for: String.self) { text in
            Text("Invalid")
        }
    }
}

画面遷移を挟んでもnavigationDestinationは有効です。
その為、遷移先のViewに同じ型のnavigationDestinationを書いても無効になります。

同じ型を使う場合は1箇所のnavigationDestinationで分岐させる形にします。

struct SampleViewA: View {
    var body: some View {
        NavigationStack {
            List{
                NavigationLink("Go to SampleB",value: "sampleB")
            }
            .navigationDestination(for: String.self) { text in
                if text == "sampleB"{
                    SampleViewB()
                    Text("Valid")
                }else{
                    Text("SampleViewC")
                    Text("Valid")
                }
            }
        }
    }
}

struct SampleViewB: View {
    var body: some View {
        List{
            NavigationLink("Go to SampleC",value: "sampleC")
        }
        .navigationDestination(for: String.self) { text in
            Text("Invalid")
        }
    }
}

NavigationViewではNavigationLinkで指定していた為、特に実装箇所は意識する必要がありませんでした。
NavigationStackはバラバラに実装すると、どのnavigationDestinationが有効かわからなくなるので、
なるべくNavigationStack直下にまとめて実装しましょう。
カスタムModifierでまとめてしまうのも1つの手です。

struct DestinationModifier:ViewModifier{
 
    func body(content: Content) -> some View {
        content.navigationDestination(for: String.self) { value in
            Text(value)
        }.navigationDestination(for:Int.self) { value in
            Text("\(value)")
        }
    }
}

///////

List{
    NavigationLink(~
}
.modifier(DestinationModifier())

enumのassociated valueを活用する

valueが同じ型でnavigationDestinationで分岐する場合、
データの中身を見て判断する形でも良いですが少々手間ですし無駄な処理も増えます。

valueに使う型を変えるために構造体を用意しても良いですが、
enumのassociated value利用するのが良いでしょう。

enumは宣言する際に引数を指定できます。
この引数で値を保持する事ができ、これをassociated valueと言います。

enum SamplePath{
    case sampleViewA(String)
    case sampleViewB(String)
}

これでNavigationLinkの識別子と必要な値をまとめて渡すことができます。
NavigationLinkとnavigationDestinationを以下の用に変更します。

NavigationLink("Go to SampleB",value: SamplePath.sampleViewA("sampleB"))
NavigationLink("Go to SampleC",value: SamplePath.sampleViewB("sampleC"))

.navigationDestination(for: SamplePath.self) { samplePath in
    switch samplePath {
    case let .sampleViewA(text):
        SampleViewB()
        Text(text)
    case let .sampleViewB(text):
        Text("SampleViewC")
        Text(text)
    }
}

switchで「case let .sampleViewA( text ):」とすると「text」の部分でassociated valueの値が受け取れます。
このようにする事で、同じ型を扱う場合でも値の中身まで確認せずとも分岐が可能です。

NavigationStackは2重にしてはいけない

1つのNavigationStackでnavigationDestinationで同じ型が使えないのであれば、
NavigationStackをもう1つ使えば良いのではないか?と考えると思います。

しかし、NavigationStackは入れ子になると上手く動作しません。
NavigationViewの時は見た目が良くないものの使用は出来たので気をつけてください。

NavigationStack {
    List{
        NavigationLink("Go to Next",value: "Next")
    }
    .navigationDestination(for: String.self) { text in
        NavigationStack {
            Text("not work well")
        }
    }
}

NavigationView {
    List{
        NavigationLink("Go to Next"){
            NavigationView {
                Text("design collapses")
            }
        }
    }
}

最後に

NavigationStackについて簡単に確認しました。
NavigationLinkのselectionを使用した画面遷移制御についてもDeprecatedとなり別な形のものとなっております。

以下の記事で紹介していますので是非ご確認ください。

SceneStoregeで画面遷移を保持する方法についても書きました。

コメント

  1. […] 【SwiftUI】NavigationStack (iOS16、iPadOS16以降)iOS16が正式にリリースされたのでNav… 【SwiftUI】NavigationStackで画面遷移をコントロールする(NavigationPath)(iOS16/iPadOS16)iOS16/iPadOS16からはNavigationViewが非推奨(Deprecated)となりました。代わりに実装されたのがNavigationStackです。これに合わせてNavigationLinkにも変更がありselectio…thwork.net2022.09.19 【SwiftUI】NavigationStackで画面遷移を維持する(NavigationPathをSceneStorage等で保存する)NavigationStackで画面遷移状態を保持し、アプリを起動し直してもセッションが残っていれば元の画面を表示できるようにする方法です。NavigationPathのデータをSceneStorageに保存し、起動時にリストアします。…thwork.net2022.09.28SwiftUIiOS16NavigationLinkNavigationStackシェアする Twitter Facebook はてブ Pocket LINE コピーthwork.devをフォローする thwork.dev thwork […]

タイトルとURLをコピーしました