【SwiftUI】NavigationStackで画面遷移をコントロールする(NavigationPath)(iOS16/iPadOS16)

SwiftUI

iOS16/iPadOS16からはNavigationViewが非推奨(Deprecated)となりました。
代わりに実装されたのがNavigationStackです。
これに合わせてNavigationLinkにも変更がありselectionの引数があるものが非推奨になりました。
画面遷移を制御するコードが大きく変わったので確認していきましょう。

画面遷移をコントロールする

NavigationStackを用意する

まずは基本となるNavigationStackを用意しましょう。

struct FruitsListView: View {
    
    let fruits = ["Apple" ,"Grape" ,"Strawberry"]
    
    var body: some View {
        NavigationStack{
            List(fruits ,id: \.self){ fruit in
                NavigationLink(fruit ,value: fruit)
            }
            .navigationDestination(for: String.self) { fruit in
                Text(fruit)
            }
            .navigationTitle("FruitsList")
        }
    }
}

まずはフルーツ一覧から1つ選択し、遷移後の画面ではテキストが表示される単純な画面です。
今回はvalueにString型を使用しています。

pathを使用して画面遷移する

NavigationStackにはpath引数があります。
これを使って画面遷移を制御して行きます。

@State var path = [String]()

NavigationStack(path: $path){

}

pathにはBindingな配列を使用する事が出来ます。
その為、@Stateでマークした変数を$つきで渡しています。

今回はNavigationLinkからnavigationDestinationに渡される値がStringなのでString配列です。
この値によって配列の型が変わります。

それでは先ほどのコードにpathを追加していきましょう。

struct FruitsListView: View {
    
    let fruits = ["Apple" ,"Grape" ,"Strawberry"]
    
    @State var path = [String]()
    
    var body: some View {
        NavigationStack(path: $path){
            List(fruits ,id: \.self){ fruit in
                NavigationLink(fruit ,value: fruit)
            }
            .navigationDestination(for: String.self) { fruit in
                Text(fruit)
            }
            .navigationTitle("FruitsList")

            Button("Test"){
                path.append("Apple")
            }
        }
    }
}

そのままpathの変数とNavigationStackの引数の変更のみです。
NavigationViewの時とは異なりNavigationLinkへの変更は不要な為、
NavigationLinkが大量にあっても作業量が増えません。

画面遷移の処理はテストボタンに実装しました。
pathにNavigationLinkから渡されるvalueを追加するだけです。
逆にpathから削除すると前の画面に戻る事が出来ます。

階層を増やす

先程はListとTextだけの画面遷移でした。
実際の場合は繰り返し画面遷移を行うことも多いです。

次はListを2回選択する形にしてみましょう。

struct FruitsListView: View {
    
    let fruits = ["Apple" ,"Grape" ,"Strawberry"]
    
    @State var path = [String]()
    
    var body: some View {
        NavigationStack(path: $path){
            List(fruits ,id: \.self){ fruit in
                NavigationLink(fruit ,value: fruit)
            }
            .navigationDestination(for: String.self) { value in
                if fruits.contains(value) {
                    VegetablesListView(fruit: value)
                }else{
                    Text(value)
                }
            }
            .navigationTitle("FruitsList")
            
            Button("Test"){
                path = ["Apple","Tomato"]
            }
        }
    }
}


struct VegetablesListView:View{
    
    let vegetables = ["Tomato" ,"Potato" ,"Carrot"]
    
    let fruit:String
    
    var body: some View{
        List(vegetables ,id: \.self){ vegetable in
            NavigationLink(vegetable ,value: vegetable)
        }
        .navigationTitle("VegetablesListView")
        
        Text("selected \(fruit)")
    }
}

「フルーツリスト→野菜リスト→Text表示」という形になっています。
同じ型を扱っているのでnavigationDestination内で分岐しています。

.navigationDestination(for: String.self) { value in
    if fruits.contains(value) {
        VegetablesListView(fruit: value)
    }else{
        Text(value)
    }
}

同じ型の場合は画面遷移を行った後の画面でも、このnavigationDestinationが使われます。
今回の場合VegetablesListView内にnavigationDestinationを実装しても無効になります。

画面遷移のテストボタンの処理は以下の様に変更しました。

Button("Test"){
    path = ["Apple","Tomato"]
}

複数画面遷移する場合も配列の中身を変えるだけです。
今回はそのまま配列を代入しました。appendを2回やっても同じで大丈夫です。

こちらも配列から削除する事で前の画面に戻る事が出来ます。
1つずつ削除して戻る事も出来ますし、空にすると最初の画面まで1度に戻る事も出来ます。

これは非常に便利で、配列の中身を書き換えるだけで戻ったり進んだりが自由に出来ます。
例えば今回の場合だとTomatoから2つ戻ってGrape→Potatoと進む処理も代入1回で済みます。

NavigationViewでこの様な遷移を行う場合、画面遷移の待ち時間を設けるか、
一度Viewを非表示にして表示しなおさないと上手く動作しませんでした。

配列操作の注意点

NavigationStackは配列の通りに画面遷移してくれます。
これは便利な点でも注意が必要な点でもあります。

配列の通りということは、UI上は不可能でも配列を直接操作すれば表示できるということです。
今回の例では、直接代入すれば[“Tomato”,”Tomato”,”Apple”,”Orange”]という様な配列にも出来ます。
しかし、UI上では初めにTomato選択することはできませんし、
そもそも画面遷移は2回まででOrangeは存在しません。

この様に、UI上からは不可能な画面を表示する事が出来てしまいます。
上手く利用する事も出来ますが、想定外の画面が表示される場合もあるので十分に気をつけましょう。

異なる型の画面遷移を制御する

画面遷移を行う際に異なる型を渡したいことはよくあります。
navigationDestination自体も型によって分岐する様に出来ています。

まずは異なる型にしたコードを用意しましょう。

enum Fruits:String,CaseIterable{
    case apple = "Apple"
    case grape = "Grape"
    case strawberry = "Strawberry"
}

enum Vegetables:String,CaseIterable{
    case tomato = "Tomato"
    case potato = "Potato"
    case carrot = "Carrot"
}

struct FruitsList2View: View {
    
    var body: some View {
        NavigationStack{
            List(Fruits.allCases ,id: \.self){ fruit in
                NavigationLink(fruit.rawValue ,value: fruit)
            }
            .navigationDestination(for: Fruits.self) { fruit in
                VegetablesList2View(fruit: fruit.rawValue)
            }
            .navigationDestination(for: Vegetables.self) { vegetable in
                Text(vegetable.rawValue)
            }
            .navigationTitle("FruitsList")
        }
    }
}

struct VegetablesList2View:View{
    
    let fruit:String
    
    var body: some View{
        List(Vegetables.allCases ,id: \.self){ vegetable in
            NavigationLink(vegetable.rawValue ,value: vegetable)
        }
        .navigationTitle("VegetablesListView")
        
        Text("selected \(fruit)")
    }
}

先ほどのコードでStringだったものをenumにしました。
これでFruitsとVegetablesが別な型になったため、pathを配列で宣言する事が出来なくなりました。

なお、この状態で片方の型に合わせてpathを使うと、もう一方で画面遷移が出来なくなります。

NavigationPathを使う

enumのassociated valueを使うとNavigationLinkのvalueを1つの型にしつつ、
異なる型のデータを渡す事も出来ますが、1つのnavigationDestination内で分岐させる事になってしまいます。

こういった時の為にNavigationPathという構造体が用意されています。
NavigationPathは型を選ばない配列の様な機能を持っており、
異なる型でもappendで追加して持たせる事が出来ます。

@State var path = NavigationPath()

path.append(Fruits.apple)
path.append(Vegetables.tomato)
path.removeLast(1)//一つ戻る
path.removeLast(path.count)//最初まで戻る

appendの他には後ろから削除していくremoveLastがあります。
removeLastは引数に幾つ削除するかを指定できますが、NavigationPathの持つデータ数を超えるとクラッシュするので気をつけてください。
NavigationPathにはcountとisEmptyがあるので、こちらを活用してください。

以下はNavigationPathを追加したコードです。

struct FruitsList2View: View {
    
    @State var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path){
            List(Fruits.allCases ,id: \.self){ fruit in
                NavigationLink(fruit.rawValue ,value: fruit)
            }
            .navigationDestination(for: Fruits.self) { fruit in
                VegetablesList2View(fruit: fruit.rawValue)
            }
            .navigationDestination(for: Vegetables.self) { vegetable in
                Text(vegetable.rawValue)
            }
            .navigationTitle("FruitsList")
            
            Button("Test"){
                path.append(Fruits.apple)
                path.append(Vegetables.tomato)
            }
        }
    }
}

NavigationPathの宣言とNavigationStackの引数に渡すだけなので非常に簡単です。
画面遷移処理は同じくテストボタンを用意しました。

NavigationPathと配列の使い分け

NavigationPathは型に縛られない為、非常に便利です。
しかし、型が統一されている時まで全てNavigationPathの方がいいとは限りません。

NavigationPathは追加と削除はできますが、中身のデータを参照し易い作りになっていません。
データが欲しい場合はViewの引数で渡したり、別途EnvironmentObjectで保持する必要があります。

配列の場合は中身のデータを参照する事が出来るので、
前の階層のデータが欲しい場合にpath配列が使えれば良い訳です。

どちらにも利点があるので適切に使い分けましょう。

最後に

NavigationViewからNavigationStackに変わる事で画面遷移の制御方法が大きく変わりました。
1つの変数で管理することなりNavigationPathが新たに追加されましたが、
一度に何階層も画面遷移さる場合には非常に楽になり、過去にあった問題も解消されました。

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

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

コメント

  1. […] 【SwiftUI】NavigationStackで画面遷移をコントロールする(NavigationPath)(iOS16/i… […]

  2. […] 【SwiftUI】NavigationStackで画面遷移をコントロールする(NavigationPath)(iOS16/i… […]

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

  4. […] 【SwiftUI】NavigationStackで画面遷移をコントロールする(NavigationPath)(iOS16/i… […]

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