[TCA] Tree-based Navigationで Destination Enumを使うべき理由
概要
画面遷移をTCAのTree-based Navigationを利用して実装したときに、遷移先が2つ以上ある場合は、以下の理由でDestination enum
を利用すべきである。
- コードが短く書ける
- 遷移用StateとViewの不整合を防げる
- 遷移用Stateの状態を理解しやすい
- 遷移用Stateのとりうるすべての状態が有効
TCAのTree-based Navigationってなんだっけ?
Destination Enumとは
ある画面に、複数の遷移先が存在する場合は、それぞれの遷移先に対してOptional State
を用意するのではなく、1つのEnum(Destination Enum)で済ます方法。
比較
State
- それぞれの遷移先に対して
Optional State
を用意する
struct State: Equatable {
// 各遷移先にStateを1つずつ
@Presents var childA: ChildA.State?
@Presents var ChildB: ChildB.State?
@Presents var ChildC: ChildC.State?
}
- 1つのEnumで済ますと
@Reducer(state: .quatable)
enum Destination {
case childA(ChildA)
case childB(ChildB)
case childC(ChildC)
}
struct state: Equatable {
@Presents var destination: Destination.State?
}
Destination Enum
を定義する必要があるが、Optional State
の数は1つに減っている。
Action
- それぞれの遷移先に対して
Optional State
を用意する
enum Action: Sendable {
case childA(PresentationAction<ChildA.Action)
case childB(PresentationAction<ChildB.Action)
case childC(PresentationAction<ChildC.Action)
}
- 1つのEnumで済ますと
enum Action: Sendable {
case destination(PresentationAction<Destination.Action>)
}
1つのcaseで済むようになる。
Reducerのbody
- それぞれの遷移先に対して
Optional State
を用意する
var body: some Reducer<State, Action> {
Reduce { state, action in
// ...
}
.ifLet(\.$childA, action: \.childA) {
ChildA()
}
.ifLet(\.$childB, action: \.childB) {
ChildB()
}
.ifLet(\.$childC, action: \.childC) {
ChildC()
}
}
- 1つのEnumで済ますと
var body: some Reducer<State, Action> {
Reduce { state, action in
// ...
}
.ifLet(\.$destination, action: \.destination)
}
1つのifLet
で済むようになる。
enum Destination
についている@Reducer
Macroのおかげで、面倒なボイラープレートなコードの量が減っている。
Destination Enumを使うべき理由
コードが短くなる以外に以下の利点がある。
UIとStateの不整合を防げる
遷移先が複数ある場合、Optional State
を使用すると、遷移先の数だけ、Optional State
を用意する必要がある。
その場合、正しく運用しないと、同時に複数の遷移先のStateがnon-nil
になってしまう可能性がある。
現在のSwiftUIでは、同時にPresentできるViewは1つなので、複数のStateがnon-nil
になることは、UIとStateの整合性が失われることになる。
それに対し、Destination Enum
では、Stateは1つなので、同時に複数のViewがPresentされることはない。
とりうる状態数が少ないため、状態を理解しやすい
複数の遷移先のStateが存在すると、現在表示されているViewが何なのか調べるのが難しくなる。
すべてのOptional Stateを調べ、non-nil
のものを見つける必要がある。また、複数のStateがnon-nil
の場合の解釈も難しい。
それに対し、Destination Enum
では、遷移用のStateは1つなので、表示されているViewを理解しやすい。
無効な状態数がない
遷移先が複数ある場合、Optional State
を使用すると、遷移先が増えるたびに、とりうる状態数が2倍になる。
とりうる状態数は、遷移先をN個とすると、
その内、真に有効なものは、
その他は、同時に複数の遷移先のStateがnon-nil
になってしまう。
それに対し、Destination Enum
では、とりうる状態数は
Destination Enum
を利用することで、コンパイル時に、同時に1つの遷移先のみが有効になることを保証できる。
結論
これらの3つの理由により、複数の遷移先が存在する場合は、それぞれの遷移先に対してOptional State
を用意するより、1つのEnumで済むDestination Enum
のほうが好まれる。
Sampleコードで理解する
以下の仕様を考える
- 商品のリストを表示する画面(ItemListFeature)
- 商品リストからは、3つの画面に遷移できる
- 商品の詳細画面(ItemDetailFeature)
- 商品の編集画面(ItemEditFeature)
- 商品の追加画面(ItemAddFeature)
Viewの実装の例
どちらの実装方法でも、Viewは大体、以下のようになる。
(Destination Enumだと、scopeのところが、scope(\.destination?.itemDetail, action: \.destination.itemDetail)
のようになる)
struct ItemListView: View {
@Bindable var store: StoreOf<ItemListFeature>
var body: some View {
NavigationStack {
// ...
}
// Drill downで ItemDetailを表示
.navigationDestination(item: $store.scope(state: \.itemDetail, action: \.itemDetail)) { store in
ItemDetailView(store: store)
}
// Drill downで ItemEditを表示
.navigationDestination(item: $store.scope(state: \.itemEdit, action: \.itemEdit)) { store in
ItemEditView(store: store)
}
// Sheetで ItemAddを表示
.sheet(item: $store.scope(state: \.itemAdd, action: \.itemAdd)) { store in
ItemAddView(store: store)
}
.toolbar {
// ...
}
}
}
Reducerの実装
Reducerの実装は2つの間で、かなり違いがある。
各遷移先にOptional Stateを用意する
@Reducer
struct ItemListFeature {
@ObservableState
struct State: Equatable {
// 各遷移先にStateを1つずつ
@Presents var itemDetail: ItemDetailFeature.State?
@Presents var itemEdit: ItemEditFeature.State?
@Presents var itemAdd: ItemAddFeature.State?
var itemList: [String] = items
}
enum Action: Sendable {
// Button interaction
case tapItem(item: String)
case tapEdit(item: String)
case tapAdd
// For Navigation
// 各遷移先にActionを1つずつ
case itemDetail(PresentationAction<ItemDetailFeature.Action>)
case itemEdit(PresentationAction<ItemEditFeature.Action>)
case itemAdd(PresentationAction<ItemAddFeature.Action>)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .tapItem(let item):
state.itemDetail = ItemDetailFeature.State(item: item)
return .none
case .tapEdit(let item):
state.itemEdit = ItemEditFeature.State(item: item)
return .none
case .tapAdd:
state.itemAdd = ItemAddFeature.State()
return .none
case .itemDetail, .itemEdit, .itemAdd:
return .none
}
}
// 各遷移先にifLetを1つずつ
.ifLet(\.$itemDetail, action: \.itemDetail) {
ItemDetailFeature()
}
.ifLet(\.$itemEdit, action: \.itemEdit) {
ItemEditFeature()
}
.ifLet(\.$itemAdd, action: \.itemAdd) {
ItemAddFeature()
}
}
Destination Enumを使用して実装する
@Reducer
struct ItemListFeature {
@Reducer(state: .equatable)
enum Destination {
// 各遷移先にcaseを1つずつ
case itemDetail(ItemDetailFeature)
case itemEdit(ItemEditFeature)
case itemAdd(ItemAddFeature)
}
@ObservableState
struct State: Equatable {
// Stateはまとめて1つ
@Presents var destination: Destination.State?
var itemList: [String] = items2
}
enum Action: Sendable {
case tapItem(item: String)
case tapEdit(item: String)
case tapAdd
// Actionはまとめて1つ
case destination(PresentationAction<Destination.Action>)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .tapItem(let item):
state.destination = .itemDetail(ItemDetailFeature.State(item: item))
return .none
case .tapEdit(let item):
state.destination = .itemEdit(ItemEditFeature.State(item: item))
return .none
case .tapAdd:
state.destination = .itemAdd(ItemAddFeature.State())
return .none
case .destination:
return .none
}
}
// ifLetはまとめて1つ, 短かく書ける
.ifLet(\.$destination, action: \.destination)
}
}
比較してみると
各遷移先にOptional Stateを用意する | Destination enumを使用する | |
---|---|---|
State | 各遷移先に1つずつ | まとめて1つ |
Action | 各遷移先に1つずつ | まとめて1つ |
ifLet | 各遷移先に1つずつ | まとめて1つ |
Reducer body | ほぼ同じ | ほぼ同じだが、少し短く書ける |
コード量 | より多い | より少ない |
その他 | Destination enumを定義する必要がある |
まとめ
画面遷移をTCAのTree-based Navigationを利用して実装したときに、遷移先が2つ以上ある場合は、以下の理由でDestination enum
を利用すべきである。
- コードが短く書ける
- 遷移のStateとViewの不整合を防げる
- 遷移のStateの状態を理解しやすい
- 遷移のStateのとりうるすべての状態が有効
Envrionment
- TCA v1.13.0
SampleCode
- 各遷移先にOptional Stateを用意する
- Destination Enumを使用して実装する例
Discussion