状態に応じてReducerを返せるReducerReaderを見る
記事にまとめるほど理解できていないので一旦スクラップで。
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に直したりすれば確認することができる。
修正すると次の通り。
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を返す。
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)
}
}
まだ入るかどうかはわからないけど、多分くる。