NavigationStackで同じクラスに複数の遷移先を与える（SwiftUI）

なかなか面白いことを思いついたので急遽記事にしています。

NavigationStackを使った画面遷移で、痒い所に手を届かせたのでそのお話し。

# 痒かったところ

NavigationStackは、自身が管理しているpathに入れた値によって、画面遷移先を決定しています。

例えば親要素のCategoryと、子要素のItemというEntityがあったとします。

- Category
-- name
-- items（子要素）
- Item
-- name
-- category（親要素）

まずは単純にItemの一覧画面。
```Swift
List{
    ForEach(items, id: \.self){ item in
        Text(item.name)
    }
}
```
タップするとItemの編集画面（UpdateItemView）に遷移するようにします。
```Swift
NavigationStack(path: $path){
    List{
        ForEach(items, id: \.self){ item in
            NavigationLink(value: item){
                Text(item.name)
            }
        }
    }
    .navigationDestination(for: Item.self) { item in
        UpdateItemView(item: item)
    }
}
```
これがNavigationStackの基本形として、次のパターンを見てみましょう。

では今度は、Categoryの一覧を作ります。
```Swift
List{
    ForEach(categories, id: \.self){ category in
        Text(category.name)
    }
}
```
タップすると、そのCategoryの子要素となるItemの一覧画面（UpdateItemView）に遷移するようにします。
```Swift
NavigationStack(path: $path){
    List{
        ForEach(categories, id: \.self){ category in
            NavigationLink(value: category){
                Text(category.name)
            }
        }
    }
    .navigationDestination(for: Category.self) { category in
        List{
            ForEach(category.items, id: \.self){ item in
                Text(item.name)
            }
        }
    }
}
```
続いて、Category自体の編集画面（UpdateCategoryView）を入れたいです。
どうすれば良いでしょうか？

タップの場合は子要素のItemを表示することになっているので、スワイプアクション（Button）で処理を追加しましょう。

```diff_swift
NavigationStack(path: $path){
    List{
        ForEach(categories, id: \.self){ category in
            NavigationLink(value: category){
                Text(category.name)
            }
+           .swipeActions{
+               Button{
+                   path.append(category)
+               } label: {
+                   Image(systemName: "pencil") // 編集なのでペンマーク
+               }
+               .tint(.green)
+           }
        }
    }
    .navigationDestination(for: Category.self) { category in
        List{
            ForEach(category.items, id: \.self){ item in
                Text(item.name)
            }
        }
    }
}
```
スワイプアクションのボタンをタップすると、path.append(category)されますが、これでは子要素のItem一覧画面に遷移してしまいますね。

そうなんです。このNavigationStackの痒いところというのは、一つの型に対し複数の遷移先を設定できないということなんです。

# 対処１（微妙）
最初はこれしか思いつきませんでした。

特別なEnumを作ります。
```Swift
enum SpecialPath{
    case update
}
```
遷移先に渡すCategoryをキャッシュして、
```Swift
@State var categoryCashe: Category?
```
特別なPathを渡して遷移先をねじ曲げます。
```diff_swift
NavigationStack(path: $path){
    List{
        ForEach(categories, id: \.self){ category in
            NavigationLink(value: category){
                Text(category.name)
            }
            .swipeActions{
                Button{
+                    categoryCashe = category
+                    path.append(SpecialPath.update)
                } label: {
                    Image(systemName: "plus")
                }
                .tint(.green)
            }
        }
    }
    .navigationDestination(for: Category.self) { category in
        List{
            ForEach(category.items, id: \.self){ item in
                Text(item.name)
            }
        }
    }
+   .navigationDestination(for: SpecialPath.self) { _ in
+       if let category = categoryCashe{
+           UpdateCategoryView(category: categoryCashe)
+       }
+   }
}
```
これで一応は遷移できるんですが、、、

正直、これだけのために新しいEnumを定義したり、しかもそのEnumは一つしか値を持っていなかったり、何よりキャッシュのために無駄なStateを持たせたり、使うときはOptionalなのでアンラップしなければならないし、とにかく気持ち悪い。

# 対処２（画期的！）
今回僕が思いついたのは、特別なEnumではなく特別なStructを作るという作戦です。

```Swift
struct CategoryUpdate: Hashable{
    var value: Category

    // 以下はHashableであるために必要な定型
    func hash(into hasher: inout Hasher) {}
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.value == rhs.value
    }
}
```
なぜHashableでなければならないのか？

それは、この構造体をpathにappendするからです！

```diff_swift
NavigationStack(path: $path){
    List{
        ForEach(categories, id: \.self){ category in
            NavigationLink(value: category){
                Text(category.name)
            }
            .swipeActions{
                Button{
-                    categoryCashe = category
+                    path.append(CategoryUpdate(value: category))
                } label: {
                    Image(systemName: "plus")
                }
                .tint(.green)
            }
        }
    }
    .navigationDestination(for: Category.self) { category in
        List{
            ForEach(category.items, id: \.self){ item in
                Text(item.name)
            }
        }
    }
+   .navigationDestination(for: CategoryUpdate.self) { category in
+       UpdateCategoryView(category: category.value)
+   }
}
```
pathをappendするときに即興でラッパーを作り、遷移先を分岐させます。

これならキャッシュのような無駄なStateを作らないのはもちろん、構造体のネーミングによって役割も明確化します。

# 終わりに

めちゃくちゃしっくり来たわけではないんですが、画期的なアイデアだと思ったので記事にしました。

賛否あればぜひコメントいただけると幸いです🙇