NavigationStackで、Homeに戻るナビゲーションの実装

# NavigationStackで、Homeに戻るナビゲーションの実装

## NavigationStack

WWDC2022にて、次期iOS(iOS16)における新しいNavigation手法として`NavigationStack`が公開されました。

これまでのNavigationViewでは、基本的に以下のページ遷移しか出来ませんでした。

1. NavigationLinkで指定のViewへ遷移する
1. 遷移先から遷移元へ一つだけ戻る

`NavigationStack`を用いることで、もう少し自由度の高いページ遷移が行えるようです。

個人的に導入を待ち望んでいた機能なので、開発者向けベータ版を導入して試してみました。


## 動作イメージ

今回は以下のように、Navigationで孫ページまで遷移したのち、一足飛びにROOTのページまで戻る実装を試してみました。

![NavigationStack_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542361/52a97375-490a-66f5-e836-b82716ddbf7e.gif)


## NavigationPath情報の確保

Navigation情報である`NavigationPath`を確保/操作するオブジェクトを定義します。

`gotoHomePage`は、Navigatinのホーム(Root)に戻る操作です。

```swift
class NavigationCoordinator: ObservableObject {
    
    @Published var path = NavigationPath()
    
    func gotoHomePage() {
        path.removeLast(path.count)
    }
}
```

## NavigationViewからNavigationStackへ置き換え

従来`NavigationView`としていた箇所を`NavigationStack`へ置き換え、上記で定義した`path:`を渡します。

従来の`NavigationLink`では表示対象と遷移先のViewを指定していましたが、ここでは`value:`として必要な情報のみ渡します。

遷移先のView情報は、`.navigationDestination`にて指定します。

この際、`for:`には上記の`value:`で渡した情報の型を指定します。

```swift
import SwiftUI

struct FirstTabView: View {
    
    @StateObject var coordinator = NavigationCoordinator()
    
    @StateObject var viewModel = FirstTabViewMode()
    
    var body: some View {
        
        NavigationStack(path: $coordinator.path) {
            
            VStack {
                List( viewModel.childs) { child in
                    
                    NavigationLink(value: child) {
                        HStack {
                            Image(systemName: "book")
                            Text(child.name)
                        }
                    }
                }
                .navigationDestination(for: ChildModel.self) { child in
                    ChildView(coordinator: coordinator,child: child)
                }
            }
        }
        .navigationViewStyle(StackNavigationViewStyle()).padding()
        .navigationTitle("First")
    }
}
```

また、指定できる型は`Hashable, Identifiable`である必要があります。

```swift
import Swift

struct ChildModel : Hashable, Identifiable {
    let id : Int
    let name : String
    let grandchildren : [GrandchildModel]
}
```

## 子供/孫のNavigationLink

上記と同様に`.navigationDestination`を利用します。

任意の箇所で`NavigationCoordinator.gotoHomePage()`を呼び出すことで、`NavigationStack`を定義したビューへ戻ることが出来ます。

```swift
import SwiftUI

struct ChildView: View {
    
    @ObservedObject var coordinator : NavigationCoordinator
    
    let child : ChildModel
    
    var body: some View {
        VStack {
            Text(child.id.description)
            Text(child.name)
            
            List(child.grandchildren) { grandchild in
                NavigationLink(grandchild.id.description, value: grandchild)
            }
            .navigationDestination(for: GrandchildModel.self) { grandchild in
                GrandchildView(coordinator: coordinator, grandchild: grandchild)
            }
        }
    }
}
```

```swift
struct GrandchildView: View {
    
    @ObservedObject var coordinator : NavigationCoordinator
    
    let grandchild : GrandchildModel
    
    var body: some View {
        VStack {
            Text(grandchild.id.description)
        }
        .navigationBarItems(
            trailing: HStack {
                Button(action: {
                    coordinator.gotoHomePage()
                }) {
                    Text("ROOT")
                }
            }
        )
    }
}
```

```swift
import Swift

struct GrandchildModel : Hashable, Identifiable {
    let id : Int
}
```

## 【追記】遷移情報の保存と復元

`NavigationPath`はcodableなので、以下のようにすれば遷移情報を保存/復元してブックマークのように扱うことも可能なようです。

(配列操作に`swift-algorighms`を使っています。)

```swift
    func savePath() {
        let data = path.codable
        do {
            let encoder = JSONEncoder()
            encoder.outputFormatting = .prettyPrinted
            let encodedData = try encoder.encode(data)
            UserDefaults.standard.set(encodedData, forKey: "bookmark")
            let jsonstr = String(data: encodedData, encoding: .utf8)!
            print(jsonstr)
        } catch let error {
            print(error)
        }
    }
    
    func loadPath() {
        guard let data = UserDefaults.standard.data(forKey: "bookmark") else {return}
        do {
            let decoder = JSONDecoder()
            let decodedData = try decoder.decode([String].self, from: data)
            let pairingData = decodedData.chunks(ofCount: 2)
            
            path.removeLast(path.count)
            
            pairingData.reversed().forEach({ onePair in
                do {
                    if (onePair.first!.contains("ChildModel")) {
                        
                        let model = try decoder.decode(ChildModel.self, from: onePair.last!.data(using: .utf8)!)
                        print("ChildModel : \(model)")
                        
                        path.append(model)
                        
                    } else if (onePair.first!.contains("GrandchildModel")) {
                        
                        let model = try decoder.decode(GrandchildModel.self, from: onePair.last!.data(using: .utf8)!)
                        print("GrandchildModel : \(model)")
                        
                        path.append(model)
                        
                    } else {
                        
                        print("unrecoginize model : \(onePair.first ?? "")")
                    }
                } catch let error {
                    print(error)
                }
            })
            
            tabSelection = 0
            
        } catch let error {
            print(error)
        }
```