Closed1

状態に応じてReducerを返せるReducerReaderを見る

morninmornin

記事にまとめるほど理解できていないので一旦スクラップで。

https://github.com/pointfreeco/swift-composable-architecture/pull/1969

PRに上がっている通りReducerReaderが追加されるかもしれない。

1つのメリットは次のように書けるようになる。

var body: some ReducerProtocol<State, Action> {
  ReducerReader { state, _ in
    switch state {  // Compile-time failure if new cases are added
    case .loggedIn:
      Scope(state: /AppFeature.loggedIn, action: /AppFeature.loggedIn) {
        LoggedInFeature()
      }
    case .loggedOut:
      Scope(state: /AppFeature.loggedOut, action: /AppFeature.loggedOut) {
        LoggedOutFeature()
      }
    }
  }
}

これは例えばオンボーディング画面でログインとログアウトしている場合で見た目を切り替える場合を考える。

これは、ログイン時のReducer。

struct LoggedInReducer: Reducer {
    struct State: Equatable {}
    enum Action: Equatable {}
    var body: some ReducerOf<Self> {
        Reduce{ state, action in
            return .none
        }
    }
}

こちらはログアウト時のReducer。

struct LoggedOutReducer: Reducer {
    struct State: Equatable {}
    enum Action: Equatable {}
    var body: some ReducerOf<Self> {
        Reduce{ state, action in
            return .none
        }
    }
}

Reducerの切り替えが重要なので機能を特に入れない。

そして、これらを状態として持つReducerをこう置く。

struct OnborardingReducer: Reducer {
    enum State: Equatable {
        case loggedIn(LoggedInReducer.State)
        case loggedOut(LoggedOutReducer.State)
        init() {
            self = .loggedOut(.init())
        }
    }
    
    enum Action: Equatable {
        case loggedIn(LoggedInReducer.Action)
        case loggedOut(LoggedOutReducer.Action)
    }
    
    var body: some ReducerOf<Self> {
        Scope(state: /State.loggedIn, action: /Action.loggedIn) {
            LoggedInReducer()
        }
        Scope(state: /State.loggedOut, action: /Action.loggedOut) {
            LoggedOutReducer()
        }
    }
}

Viewとしては次のようになる。

import ComposableArchitecture
import SwiftUI

struct LoggedInView: View {
    let store: StoreOf<LoggedInReducer>
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            Text("LoggedIn")
        }
    }
}

struct LoggedOutView: View {
    let store: StoreOf<LoggedOutReducer>
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            Text("LoggedOut")
        }
    }
}

struct OnborardingView: View {
    
    let store: StoreOf<OnborardingReducer> = .init(initialState: .init(), reducer: { OnborardingReducer() })
    
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            VStack {
                SwitchStore(store) { state in
                    switch state {
                    case .loggedIn:
                        CaseLet(/OnborardingReducer.State.loggedIn,
                                 action: OnborardingReducer.Action.loggedIn) {
                            LoggedInView(store: $0)
                        }
                    case .loggedOut:
                        CaseLet(/OnborardingReducer.State.loggedOut,
                                 action: OnborardingReducer.Action.loggedOut) {
                            LoggedOutView(store: $0)
                        }
                    }
                }
            }
        }
    }
}

これをenumで管理することでより明確にすることができる。

実際に試してみるために、readerブランチを取ってくるが古すぎてビルドが通らない。

修正自体は、新たに追加されたReducerReaderのプロトコルを追加するだけで良いので、これをソースコード内の適当なところに貼り付けて、ReducerProcolをReducerに直したりすれば確認することができる。

https://github.com/pointfreeco/swift-composable-architecture/pull/1969/files#diff-ad40a6b7c6e7f59b3431b2d324fc8ca8167cd68800fee904eadfd928dba53db1

修正すると次の通り。

public struct ReducerReader<State, Action, Reader: Reducer>: Reducer
where Reader.State == State, Reader.Action == Action {
  @usableFromInline
  let reader: (State, Action) -> Reader

  /// Initializes a reducer that builds a reducer from the current state and action.
  ///
  /// - Parameter reader: A reducer builder that has access to the current state and action.
  @inlinable
  public init(@ReducerBuilder<State, Action> _ reader: @escaping (State, Action) -> Reader) {
    self.init(internal: reader)
  }

  @usableFromInline
  init(internal reader: @escaping (State, Action) -> Reader) {
    self.reader = reader
  }

  @inlinable
    public func reduce(into state: inout State, action: Action) -> Effect<Action> {
    self.reader(state, action).reduce(into: &state, action: action)
  }
}

コレを適当なところに定義しておいて、先ほどのReducerを書き直す。

struct OnborardingReducer: Reducer {
   ...
    var body: some ReducerOf<Self> {
       ReducerReader { state, action in
           switch state {
           case .loggedIn:
               Scope(state: /State.loggedIn, action: /Action.loggedIn) {
                   LoggedInReducer()
               }
       
           case .loggedOut:
               Scope(state: /State.loggedOut, action: /Action.loggedOut) {
                   LoggedOutReducer()
               }
           }
       }
    }
}

なるほど。めちゃくちゃわかりやすくなった気がする。

比較してみよう。

    // Reduce
    var body: some ReducerOf<Self> {
        Scope(state: /State.loggedIn, action: /Action.loggedIn) {
            LoggedInReducer()
        }
        Scope(state: /State.loggedOut, action: /Action.loggedOut) {
            LoggedOutReducer()
        }
    }

    // ReducerReader
    var body: some ReducerOf<Self> {
       ReducerReader { state, action in
           switch state {
           case .loggedIn:
               Scope(state: /State.loggedIn, action: /Action.loggedIn) {
                   LoggedInReducer()
               }
       
           case .loggedOut:
               Scope(state: /State.loggedOut, action: /Action.loggedOut) {
                   LoggedOutReducer()
               }
           }
       }
    }

このReducerReaderの最もわかりやすい解説は次に示されている。

Reduce { state, action in
  // You produce an `EffectTask` here (or `Effect` soon)
}
ReducerReader { state, action in
  // You produce a `ReducerProtocol` here (or `Reducer` soon)
}

ReduceはEffectTaskを返すけれど、ReducerReaderはReducerを返す。

https://github.com/pointfreeco/swift-composable-architecture/pull/1969#issuecomment-1464972516

stateとactionを見ることができるので、それでフィルタリングもできる。

var body: some ReducerProtocol<State, Action> {
  ReducerReader { state, action in
    if state.isAdmin && action.isAdmin {
      AdminFeature()
    }
  }
}

もう一つのメリットはDependencyによる状態共有。
コレまでは昨日の記事で示したようにClientからの状態共有によって伝播させるという手法があったが、このように伝播させることができるようになる(場合によっては状態共有の方法が変えられる)

var body: some ReducerProtocol<State, Action> {
  ReducerReader { state, _ in
    Feature()
      .dependency(\.apiClient, state.isOnboarding ? .onboarding : .liveValue)
  }
}

まだ入るかどうかはわからないけど、多分くる。

このスクラップは2023/09/09にクローズされました