[SwiftUI][TCA] pullback/combine/scope
概要
この記事ではTCA初心者の筆者が理解を深めていくために、
pointfreeco
公式のサンプルアプリを基に理解しやすく整理していきます。
今回はpullback・combine・scopeといったメソッドの使用例を整理していきます。
今回扱うファイル
今回は公式サンプルのこちらのファイルです。
Composition-TwoCounters
ここでは主にpullback
とcombine
というメソッドでReducer
を合成したり、
scope
を使用しグローバルなstore
をローカルなstore
に変換しています。
State,Action
struct TwoCountersState: Equatable {
var counter1 = CounterState()
var counter2 = CounterState()
}
enum TwoCountersAction {
case counter1(CounterAction)
case counter2(CounterAction)
}
Reducer
今回Reducer
では前回のCounterアプリのcounterReducerを使用していて、
その2つのReducer
を1つにまとめる事が出来る、
以下のpullback
とcombine
いうメソッドについて説明します。
let twoCountersReducer = Reducer<TwoCountersState, TwoCountersAction, TwoCountersEnvironment>
.combine(
counterReducer.pullback(
state: \TwoCountersState.counter1,
action: /TwoCountersAction.counter1,
environment: { _ in CounterEnvironment() }
),
counterReducer.pullback(
state: \TwoCountersState.counter2,
action: /TwoCountersAction.counter2,
environment: { _ in CounterEnvironment() }
)
)
pullback
pullback
について公式の詳細なドキュメントです。
ドキュメントの説明には以下のように記載があります。
ローカルの状態、アクション、環境に対して動作するリデューサを、グローバルな状態、アクション、環境に対して動作するリデューサに変換します。これは、メソッドに3つの変換を提供することで実現されます。
グローバル状態からローカル状態の一部を取得/設定することができる書き込み可能なキーパス。
ローカルなアクションをグローバルなアクションに抽出/埋め込むことができるケースパス。
グローバル環境をローカル環境に変換する関数。
この操作は、大きなリデューサを小さなリデューサに分解するために重要である。combine(_:)-1ern2演算子と一緒に使うと、ドメインの小さな断片に対して動作する多くのリデューサを定義し、それらを引き戻して大きなドメインに対して動作する一つの大きなリデューサに結合することができます。
今回の実装で整理すると、
現状のまま参照すればもちろんcounterReducerの型は、
Reducer<CounterState, CounterAction, CounterEnvironment>
なので、
Reducer<TwoCountersState, TwoCountersAction, TwoCountersEnvironment>
に変換する必要があります。
counterReducerに対してpullback
メソッドを使用することで上記のReducer
に変換出来ます。
pullback
という命名の通り、
既存生成されているReducer
を1回元に戻す(init)ことで、
変換したいReducer
に必要な引数である、
State
・Action
・Environment
を新たに指定することで変換するイメージです。
combine
combine
について公式の詳細なドキュメントです。
ドキュメントの説明には以下のように記載があります。
多くのリデューサを1つにまとめる。
ここで重要なのは、リデューサーの組み合わせの順番が重要であるということです。reducerA と reducerB を組み合わせることは、reducerB と reducerA を組み合わせることと同じとは限りません。
これは、ドメインが重複しているリデューサを扱うときに問題になることがあります。たとえば、reducerAがreducerBのドメインを埋め込んで、そのアクションに反応したり、その状態を変更したりする場合、reducerAがreducerBの実行前にreducerBの状態を変更することを選択するか、reducerBの実行後に変更するかで違いが生じることがあるのです。
これは、おそらく optional(file:line:) のリデューサを扱うときに最もわかりやすいと思います。この場合、親ドメインは子ドメインをリスニングしてその状態をnil outすることができます。親リデューサが子リデューサより先に実行されると、 子リデューサは自分自身のアクションに反応することができません。
親リデューサを子リデューサより先に実行すると、アプリケーションのロジックエラーと見なされ、アサーションに失敗する可能性があります。そのため、ほとんどの場合、子ドメインから親ドメインへ順にreducerを組み合わせる必要があります。
Reducer<TwoCountersState, TwoCountersAction, TwoCountersEnvironment>
このReducer
への変換に成功しました。
次はこの2つのReducer
を統合し、
1つのReducer
にするために必要なメソッドがcombine
になります。
今回は2つでしたが引数にReducer
の配列を指定することも可能です。
View,Store
ここのポイントは、self.store.scope
の処理です。
共通のCounterView
というコンポーネントを使いますが、
TwoCountersState
とTwoCountersAction
には今回複数のケースがあります。
それらを区別し使用したいState
とAction
を引数に指定します。
今回は変数counter1
もcounter2
も同じCounterState
ですが、
グローバルなTwoCountersState
に別のstate
が定義されている場合などは、
使用したいローカルなstate
を指定するのによく使えると思います。
struct TwoCountersView: View {
let store: Store<TwoCountersState, TwoCountersAction>
var body: some View {
Form {
Section(header: Text(template: readMe, .caption)) {
HStack {
Text("Counter 1")
CounterView(
store: self.store.scope(state: \.counter1, action: TwoCountersAction.counter1)
)
.buttonStyle(.borderless)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
}
HStack {
Text("Counter 2")
CounterView(
store: self.store.scope(state: \.counter2, action: TwoCountersAction.counter2)
)
.buttonStyle(.borderless)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
}
}
}
.navigationBarTitle("Two counter demo")
}
}
次回
次回はbindingの使用を理解していきます。
記事はこちら
Discussion