0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-09-06

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

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

痒かったところ

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

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

  • Category
    -- name
    -- items(子要素)
  • Item
    -- name
    -- category(親要素)

まずは単純にItemの一覧画面。

List{
    ForEach(items, id: \.self){ item in
        Text(item.name)
    }
}

タップするとItemの編集画面(UpdateItemView)に遷移するようにします。

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の一覧を作ります。

List{
    ForEach(categories, id: \.self){ category in
        Text(category.name)
    }
}

タップすると、そのCategoryの子要素となるItemの一覧画面(UpdateItemView)に遷移するようにします。

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)で処理を追加しましょう。

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の痒いところというのは、一つの型に対し複数の遷移先を設定できないということなんです。

対処1(微妙)

最初はこれしか思いつきませんでした。

特別なEnumを作ります。

enum SpecialPath{
    case update
}

遷移先に渡すCategoryをキャッシュして、

@State var categoryCashe: Category?

特別なPathを渡して遷移先をねじ曲げます。

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なのでアンラップしなければならないし、とにかく気持ち悪い。

対処2(画期的!)

今回僕が思いついたのは、特別なEnumではなく特別なStructを作るという作戦です。

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するからです!

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を作らないのはもちろん、構造体のネーミングによって役割も明確化します。

終わりに

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

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

0
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?