💨

パート4 : TCAでactionをsendして実行されるまで

2023/08/28に公開

今回の流れ

  • Reducerによるstateの更新
    1. はじめに
    2. ScopedReducerによるreduce
    3. stateの更新
    4. まとめ

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: &currentState, 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: &currentState, 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