パート4 : TCAでactionをsendして実行されるまで
今回の流れ
- Reducerによるstateの更新
- はじめに
- ScopedReducerによるreduce
- stateの更新
- まとめ
Reducerによるstateの更新
手元のXcodeか、ComposableArchitectureのStoreの実装をご確認ください。
Store型にあるsendの実装を確認していきます。今回はreducerによるstateの更新という観点から見ていきます。
@_spi(Internals)
public func send(
_ action: Action,
originatingFrom originatingAction: Action?
) -> Task<Void, Never>? {
...
var index = self.bufferedActions.startIndex
while index < self.bufferedActions.endIndex {
defer { index += 1 }
let action = self.bufferedActions[index]
// reducerによるstateの更新
let effect = self.reducer.reduce(into: ¤tState, action: action)
...
}
...
}
はじめに
Reducerを仮にこのように実装したとしましょう。 ボタンが押されたらカウンターをインクリメントするだけの実装です。私たちが普段実装する時はこのreduce関数の中でactionを判断してstateを更新するという点にもっぱら集中しています。このreduceがどのように呼ばれstateが更新するのかを明らかにするのが今回のポイントです。
struct AppReducer: Reducer {
struct State: Equatable {
var count: Int = .zero
public init() {}
}
enum Action: Equatable {
case buttonTap
}
func reduce(into state: inout State, action: Action) -> ComposableArchitecture.Effect<Action> {
switch action {
case .buttonTap:
state.count += 1
return .none
}
}
}
前回までのパートでactionがviewStoreからsendされると、ViewStoreからStoreを介してsend関数が呼ばれることまでわかっています。
また、send関数では、再入可能アクションという概念が存在し、それらの処理がこのsendの冒頭にあることも確認しています。
そして、実際にstateが更新される下のreduce関数の呼び出しを見る前にこのreducerがScopedReducerであることからその実装を確認していたのでした。
// reducerによるstateの更新
let effect = self.reducer.reduce(into: ¤tState, action: action)
ScopedReducerによるreduce
手元のXCodeで ⌘ + Shift + O でScopedReducerを検索するか、GithubでScopedReducerを確認してください。ScopedReducerのreduce関数を確認します。
@inlinable
func reduce(into state: inout State, action: Action) -> Effect<Action> {
self.isSending = true
defer {
state = self.toScopedState(self.rootStore.state.value)
self.isSending = false
}
if let action = self.fromScopedAction(state, action),
let task = self.rootStore.send(action, originatingFrom: nil)
{
return .run { _ in await task.cancellableValue }
} else {
return .none
}
}
ここで重要になるのはこの部分です。
if let action = self.fromScopedAction(state, action),
let task = self.rootStore.send(action, originatingFrom: nil)
{
return .run { _ in await task.cancellableValue }
}
前回見たようにScopedReducerはそのルートのStoreを保持することでsendの再起的な呼び出しを回避するような仕組みになっています。今回はそのようなチェーンを気にすることなく、単にはじめに示したAppReducerとAppReducer.Stateを持つStoreがあるのみです。従ってこのrootStoreは次のようなStoreとなります。
let store: StoreOf<AppReducer> = .init(initialState: .init(), reducer: { AppReducer() })
このrootStoreから再度sendされると、そのStoreが持つreducerは自分たちが実装したものになります。
stateの更新
では、最初に示したAppReducerの実装を振り返りましょう。
func reduce(into state: inout State, action: Action) -> ComposableArchitecture.Effect<Action> {
switch action {
case .buttonTap:
state.count += 1
return .none
}
}
2回目にコールされたsendによってreduce関数が呼ばれますが、このReducerは上に示したAppReducerのreducerになります。actionはbuttonTapによるcount変数のインクリメントしかありませんので、ここでcurrentStateが更新されます。
この時点では、まだ再描画は発生しません。Viewに紐づいているStateの更新は、2回目のreduce関数の呼び出しを抜け、1回目のreduce関数の呼び出しが完了し、state.valueが更新されたタイミングです。reduce関数のdefer文を再度確認しておきましょう。値の更新が行われる行が確認できるはずです。
defer {
withExtendedLifetime(self.bufferedActions) {
self.bufferedActions.removeAll()
}
// state.valueの更新
self.state.value = currentState
self.isSending = false
if !self.bufferedActions.isEmpty {
if let task = self.send(
self.bufferedActions.removeLast(), originatingFrom: originatingAction
) {
tasks.wrappedValue.append(task)
}
}
}
今度はViewStoreのコードを開いてください。ViewStoreはカスタムのObservableObjectで、ここでキャッチした値の変更によってViewの再描画が行われるのでした。
public init<State>(
_ store: Store<State, ViewAction>,
observe toViewState: @escaping (_ state: State) -> ViewState,
removeDuplicates isDuplicate: @escaping (_ lhs: ViewState, _ rhs: ViewState) -> Bool
) {
...
self.viewCancellable = store.state
.map(toViewState)
.removeDuplicates(by: isDuplicate)
.sink { [weak objectWillChange = self.objectWillChange, weak _state = self._state] in
guard let objectWillChange = objectWillChange, let _state = _state else { return }
objectWillChange.send()
_state.value = $0
}
}
reducer関数のdefer内にある下の箇所が呼び出されたタイミングで、sink内のクロージャーが発火してViewの再描画がおこなわれることがわかります。
self.state.value = currentState
まとめ
パート1 ~ 3まででactionがsendされるまでに関連するクラスや構造体を見ました。それらの知識を前提に今回は、まず。ScopedReducerによるreduce関数から私たちが実装したreducerのreduce関数が呼び出されていることを確認しました。次いでそのreduce関数によってstateが更新されViewが更新されるまでの流れを確認しました。
これでactionをsendして実行されるまでのうち大部分の仕組みを理解できました。
最後に副作用の処理を確認して終わりにしましょう。
Discussion