【SwiftUI】NavigationStackで画面遷移をコントロールする(NavigationPath)(複数階層)

未分類

新たに書き直した為、この記事ではなく以下の記事を確認してください。

iOS16/iPadOS16以降でNavigationStackが追加されNavigationViewがDeprecatedになりました。
合わせてNavigationLinkのselection引数があるものもDeprecatedとなりました。

NavigationStackではUIでの操作は変わりませんが、コード側で画面遷移をコントロールする方法が大きく変わりました。
複数階層を遷移させる場合は今まで通りとはいかないので、しっかり確認しておきましょう。

複数階層のNavigationStack

まずは基本形となる複数階層になっているNavigationStackのコードからです。

内容としては単純な1〜3の数字のリストが延々と続く形です。
選択した数字は次のnavigationTitleに表示しています。

struct NumberNavigationStackView: View {
    
    var body: some View {
        NavigationStack{
            NumberListView()
                .navigationDestination(for: Int.self) { number in
                    NumberListView(number)
                }
        }
    }
}


struct NumberListView: View {
    
    let numbers = 1...3
    var navigationTitle = "NumberList"
    
    init(_ selectedNumber:Int? = nil){
        if let number = selectedNumber {
            navigationTitle = String(number)
        }
    }
    
    var body: some View {
        List(numbers ,id: \.self){ number in
            NavigationLink("\(number)" ,value: number)
        }
        .navigationTitle(navigationTitle)
    }
}

NavigationStackの特徴としてはnavigationDestinationに遷移先を指定しています。
NavigationViewの時はNavigationLinkに指定していたのでここが大きく変わった点になります。

NavigationStackの画面遷移を制御する

NavigationStackの画面遷移を制御するにはNavigationStackの引数pathを使用します。
pathには今回はBindingな配列を使用します。

pathに使用するために@Stateでマークした配列を宣言します。
今回はnavigationDestinationに渡すのがIntの為Intの配列とします。

引数はBindingなので$をつけて渡します。

実際に遷移させるにはpathの配列を変更します。
今回はボタンを用意してpathを変更しています。

struct NumberNavigationStackView: View {
    
    @State private var path: [Int] = []
    
    var body: some View {
        NavigationStack(path: $path){
            NumberListView()
                .navigationDestination(for: Int.self) { number in
                    NumberListView(number)
                }
            
            Button("Move"){
                path = [1,2,3,2,1]
            }
        }
    }
}

今回、pathの配列にはまとめて5つ入れています。
このようにまとめて遷移させる事ができるのが特徴です。
配列から値を削除する事で戻す事もでき、中間の値だけ削除しても問題ありませんし、
配列を空にして最初の画面まで戻す事も出来ます。

NavigationViewの場合は1階層に1つ変数があった為、
まとめて遷移させる場合もそれぞれの変数を変更する必要がありました。

またNavigationViewでは画面を表示したまま一度に階層を戻したり進めたりすると、
上手く画面遷移出来ない事がありました。
この様な場合には、画面遷移に待ち時間を設けるか、
一度画面を非表示にするなどして対応する必要がありました。
しかし、NavigationStackでは配列の値を書き換えるだけで、
目的の画面までの階層に関係なく一度に画面遷移する事が出来ます。

遷移先の画面によって渡す型を変えたい場合

NavigationViewではそれぞれの階層で独立していたので気にする必要はありませんでした。
しかし、NavigationStackでは全ての階層を1つの配列で管理します。

あるViewにはInt、あるViewにはString、自作したクラスや構造体を渡したい場合もあります。
例えばフォルダ一覧、データ一覧、詳細といった画面構成になる場合には、
それぞれ階層毎に遷移先には全く異なる構造体を渡したくなります。

こう言った場合にはそのまま1つの配列に入れることが出来ないので、
上手く収める工夫が必要になってきます。

遷移先に渡す型が異なるコード

分かりやすく数字を全てenum化して3階層とも別な型にしました。
まだ画面遷移を制御するコードは入っていません。

enumである事に深い意味はありません。しいて言うなら準備が楽そうだったからです。

enum First:Int,CaseIterable {
    case one = 1
    case two
    case three
}

enum Second:Int,CaseIterable {
    case one = 1
    case two
    case three
}

enum Third:Int,CaseIterable {
    case one = 1
    case two
    case three
}

struct EnumNavigationStackView: View {
    
    var body: some View {
        NavigationStack{
            FirstListView()
                .navigationDestination(for: First.self) { first in
                    SecondListView()
                }
                .navigationDestination(for: Second.self) { second in
                    ThirdListView()
                }
                .navigationDestination(for: Third.self) { third in
                    Text( "\(third.rawValue)")
                }
        }
    }
}

struct FirstListView:View{
    var body: some View{
        List( First.allCases ,id: \.self){ first in
            NavigationLink( "\(first.rawValue)" ,value: first)
        }
        .navigationTitle("FirstListView")
    }
}

struct SecondListView:View{
    var body: some View{
        List( Second.allCases ,id: \.self){ second in
            NavigationLink( "\(second.rawValue)" ,value: second)
        }
        .navigationTitle("SecondListView")
    }
}

struct ThirdListView:View{
    var body: some View{
        List( Third.allCases ,id: \.self){ third in
            NavigationLink( "\(third.rawValue)" ,value: third)
        }
        .navigationTitle("ThirdListView")
    }
}

型が3つになったのでnavigationDestinationが3つになっています。
valueに渡された型よって振り分けが行われる為、遷移は問題なく行われます。
しかし3つenumは別な型なので1つの配列に収める事が出来ません。

NavigationPathを使用する

NavigationStackのpath用にNavigationPathという構造体が用意されています。

NavigationPathはHashableな値を型に関係なく収められる配列の様な機能を持っています。
これひとつでNavigationStackでいくつ型が増えても対応する事が出来ます。

struct EnumNavigationStackView: View {
    
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            FirstListView()
                .navigationDestination(for: First.self) { first in
                    SecondListView()
                }
                .navigationDestination(for: Second.self) { second in
                    ThirdListView()
                }
                .navigationDestination(for: Third.self) { third in
                    Text( "\(third.rawValue)")
                }
            
            Button("Move"){
                path.append(First.three)
                path.append(Second.two)
                path.append(Third.one)
            }
        }
    }
}

@StateでNavigationPathを宣言しNavigationStackの引数に$付きで渡すのは同様です。
画面遷移を行う際は配列の用にappendする事が出来ます。

最後に

NavigationViewからNavigationStackに変わる事で画面遷移の制御方法が大きく変わりました。
1つの配列で管理することになった為、上手く1つの配列に収める工夫が必要になりましたが、
一度に何階層も画面遷移さる場合には非常に楽になり、過去にあった問題も解消されました。

今後iOS16以降対応のアプリを作る為にも、是非覚えていってください。

以下蛇足

最初はNavigationPathの存在に気づかずenumで対応するという方法をとっていました。

NavigationPathを使用すれば良いだけなので完全に蛇足ではありますが、
何らかの役に立つかもしれないので念の為残しておきます。

pathに使用するenumの配列を使用する

今回必要となるのは、1つの配列に収める事ができ異なる型を保持できるものです。
クラスや構造体で色々考えながら作る事も出来ますが、enumで適した使い方をする事が可能です。

enum SamplePath:Hashable{
    case first(First)
    case second(Second)
    case third(Third)
}

Swiftのenumにはassociated valueというものがあり値を保持する事が出来ます。
配列としてはenumとして1つの型として扱う事ができ、異なる型を保持する事も出来ます。
カッコの中に保持したい型を指定しましょう。

enumには遷移先のViewの種類に合わせて要素を用意しましょう。
同じ型を扱う場合にもNavigationLinkのvalueに渡す時点で判別して良いのであれば、
別な要素を用意しておくといいでしょう。

また、NavigationStackのpath引数に使用する為にはHashableが必要なので忘れないようにしましょう。

画面遷移の制御を行う

NavigationStack、navigationDestination、NavigationLinkそれぞれに変更が入ります。

まずはNavigationStack、navigationDestinationです。

NavigationStackの引数には先程用意したenumのSamplePathの配列を使用します。navigationDestinationはSamplePathを受け取る様にします。
表示するViewはswitchで切り替えます。
Viewに渡したいデータはcase let .first(first):の形で書くとカッコ内のfirstで受け取る事が出来ます。

画面遷移は同じくpathの配列をボタンで変更する形で実装しました。

struct EnumNavigationStackView: View {
    
    @State private var path: [SamplePath] = []
    
    var body: some View {
        NavigationStack(path: $path) {
            FirstListView()
                .navigationDestination(for: SamplePath.self) { samplePath in
                    switch samplePath {
                    case let .first(first):
                        SecondListView()
                        Text("\(first.rawValue)")
                    case let .second(second):
                        ThirdListView()
                        Text("\(second.rawValue)")
                    case let .third(third):
                        Text( "\(third.rawValue)")
                    }
                }
            
            Button("Move"){ 
                path = [.first(.three) ,.second(.two) ,.third(.one)]
            }
        }
    }
}

次にNavigationLinkの変更です。

NavigationLinkのvalueもpathに合わせて変更となります。
先程作ったSamplePathの該当する要素を指定し、
引数として実際にViewに渡したいデータを渡します。

3つのViewのNavigationLinkにこの変更を加えてください。

struct FirstListView:View{
    var body: some View{
        List( First.allCases ,id: \.self){ first in
            NavigationLink( "\(first.rawValue)" ,value: SamplePath.first(first)) //変更
        }
        .navigationTitle("FirstListView")
    }
}

この様な形で実装することでViewに渡す型が異なっていても、
上手く画面遷移を制御する事が可能です。

画面遷移先や型が増えてもenumに対応する値を増やすだけで済みます。

コード全体

以下はコード全体です。

struct EnumNavigationStackView: View {
    
    @State private var path: [SamplePath] = [] //追加
    
    var body: some View {
        NavigationStack(path: $path) { //変更
            FirstListView()
                .navigationDestination(for: SamplePath.self) { samplePath in //変更
                    switch samplePath {
                    case let .first(first):
                        SecondListView()
                        Text("\(first.rawValue)")
                    case let .second(second):
                        ThirdListView()
                        Text("\(second.rawValue)")
                    case let .third(third):
                        Text( "\(third.rawValue)")
                    }
                }
            
            Button("Move"){ //追加
                path = [.first(.three) ,.second(.two) ,.third(.one)]
            }
        }
    }
}

struct FirstListView:View{
    var body: some View{
        List( First.allCases ,id: \.self){ first in
            NavigationLink( "\(first.rawValue)" ,value: SamplePath.first(first)) //変更
        }
        .navigationTitle("FirstListView")
    }
}

struct SecondListView:View{
    var body: some View{
        List( Second.allCases ,id: \.self){ second in
            NavigationLink( "\(second.rawValue)" ,value: SamplePath.second(second)) //変更
        }
        .navigationTitle("SecondListView")
    }
}

struct ThirdListView:View{
    var body: some View{
        List( Third.allCases ,id: \.self){ third in
            NavigationLink( "\(third.rawValue)" ,value: SamplePath.third(third)) //変更
        }
        .navigationTitle("ThirdListView")
    }
}

コメント

  1. […] 【SwiftUI】NavigationStackで画面遷移をコントロールする2(複数階層)iOS16/iPadO…SwiftUIiOS16NavigationLinkNavigationStackNavigationViewSwiftUIシェアする Twitter Facebook はてブ Pocket LINE コピーthwork.devをフォローする thwork.dev thwork […]

  2. […] 【SwiftUI】NavigationStackで画面遷移をコントロールするiOS16/iPadOS16からはNavigationViewが非推奨(Deprecated)となりました。代わりに実装されたのがNavigationStackです。これに合わせてNavigationLinkにも変更がありdestinat…thwork.net2022.09.13 【SwiftUI】NavigationStackで画面遷移をコントロールする2(複数階層)iOS16/iPadO… […]

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