なかなか面白いことを思いついたので急遽記事にしています。
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を作らないのはもちろん、構造体のネーミングによって役割も明確化します。
終わりに
めちゃくちゃしっくり来たわけではないんですが、画期的なアイデアだと思ったので記事にしました。
賛否あればぜひコメントいただけると幸いです🙇