From 559647b8c6c93d07adb7e924a8408931b9bc967b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 8 Mar 2023 11:13:28 -0800 Subject: [PATCH 1/3] Add `ReducerReader` for building reducers out of state and action We've experimented with this before and called it `_Observe`, but weren't using it. More and more community members have built their own versions of `_Observe` and called them `StateReader` and `ActionReader`, etc., which evokes `ScrollViewReader` and `GeometryReader` nicely, so let's consider shipping this tool as a first-class citizen. A couple example uses: ```swift var body: some ReducerProtocol { 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() } } } } ``` ```swift var body: some ReducerProtocol { ReducerReader { state, action in if state.isAdmin && action.isAdmin { AdminFeature() } } } ``` We'd love any feedback the community may have, especially from those that have used this kind of reducer in their applications. We think a single `ReducerReader` entity is the way to go vs. three reducers, one for reading state _and_ action (`ReducerReader`), and then one for just reading state (`StateReader`) and one for just reading actions (`ActionReader`), since you can simply ignore the value you don't need to read: ```swift // Instead of: StateReader { state in /* ... */ } // Do: ReducerReader { state, _ in /* ... */ } // Instead of: ActionReader { action in /* ... */ } // Do: ReducerReader { _, action in /* ... */ } ``` --- .../Reducer/Reducers/ReducerReader.swift | 24 ++++++++++++++ .../ReducerBuilderTests.swift | 32 +++++++++++++------ .../ReducerReaderTests.swift | 28 ++++++++++++++++ 3 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 Sources/ComposableArchitecture/Reducer/Reducers/ReducerReader.swift create mode 100644 Tests/ComposableArchitectureTests/ReducerReaderTests.swift diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/ReducerReader.swift b/Sources/ComposableArchitecture/Reducer/Reducers/ReducerReader.swift new file mode 100644 index 000000000000..1eb5e12bedba --- /dev/null +++ b/Sources/ComposableArchitecture/Reducer/Reducers/ReducerReader.swift @@ -0,0 +1,24 @@ +/// A reducer that builds a reducer from the current state and action. +public struct ReducerReader: ReducerProtocol +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 _ 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) -> EffectTask { + self.reader(state, action).reduce(into: &state, action: action) + } +} diff --git a/Tests/ComposableArchitectureTests/ReducerBuilderTests.swift b/Tests/ComposableArchitectureTests/ReducerBuilderTests.swift index 4553edcb5cf9..5a56c5cfa04b 100644 --- a/Tests/ComposableArchitectureTests/ReducerBuilderTests.swift +++ b/Tests/ComposableArchitectureTests/ReducerBuilderTests.swift @@ -191,20 +191,32 @@ private struct Root: ReducerProtocol { #if swift(>=5.7) var body: some ReducerProtocol { - Scope(state: /State.featureA, action: /Action.featureA) { - Feature() - } - Scope(state: /State.featureB, action: /Action.featureB) { - Feature() + ReducerReader { state, _ in + switch state { + case .featureA: + Scope(state: /State.featureA, action: /Action.featureA) { + Feature() + } + case .featureB: + Scope(state: /State.featureB, action: /Action.featureB) { + Feature() + } + } } } #else var body: Reduce { - Scope(state: /State.featureA, action: /Action.featureA) { - Feature() - } - Scope(state: /State.featureB, action: /Action.featureB) { - Feature() + ReducerReader { state, _ in + switch state { + case .featureA: + Scope(state: /State.featureA, action: /Action.featureA) { + Feature() + } + case .featureB: + Scope(state: /State.featureB, action: /Action.featureB) { + Feature() + } + } } } #endif diff --git a/Tests/ComposableArchitectureTests/ReducerReaderTests.swift b/Tests/ComposableArchitectureTests/ReducerReaderTests.swift new file mode 100644 index 000000000000..6d90c0faef06 --- /dev/null +++ b/Tests/ComposableArchitectureTests/ReducerReaderTests.swift @@ -0,0 +1,28 @@ +import ComposableArchitecture +import XCTest + +@MainActor +final class StateReaderTests: XCTestCase { + func testDependenciesPropagate() async { + struct Feature: ReducerProtocol { + struct State: Equatable {} + + enum Action: Equatable { + case tap + } + + @Dependency(\.date.now) var now + + var body: some ReducerProtocolOf { + ReducerReader { _, _ in + let _ = self.now + } + } + } + + let store = TestStore(initialState: Feature.State(), reducer: Feature()) { + $0.date.now = Date(timeIntervalSince1970: 1234567890) + } + await store.send(.tap) + } +} From 1a120ca73507d76473a81820904dc6aecaa97c4b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 8 Mar 2023 11:35:11 -0800 Subject: [PATCH 2/3] wip --- .../Documentation.docc/Extensions/ReducerProtocol.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocol.md b/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocol.md index d695ee894d15..547f9399d94f 100644 --- a/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocol.md +++ b/Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocol.md @@ -24,6 +24,7 @@ - ``Reduce`` - ``CombineReducers`` - ``EmptyReducer`` +- ``ReducerReader`` - ``BindingReducer`` ### Reducer modifiers From b6d37470dc67aae2f2310fc359474aa1b022fc4d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 8 Mar 2023 11:54:04 -0800 Subject: [PATCH 3/3] fix --- .../ReducerReaderTests.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Tests/ComposableArchitectureTests/ReducerReaderTests.swift b/Tests/ComposableArchitectureTests/ReducerReaderTests.swift index 6d90c0faef06..00f65c5c6ed2 100644 --- a/Tests/ComposableArchitectureTests/ReducerReaderTests.swift +++ b/Tests/ComposableArchitectureTests/ReducerReaderTests.swift @@ -13,11 +13,19 @@ final class StateReaderTests: XCTestCase { @Dependency(\.date.now) var now - var body: some ReducerProtocolOf { - ReducerReader { _, _ in - let _ = self.now + #if swift(>=5.7) + var body: some ReducerProtocol { + ReducerReader { _, _ in + let _ = self.now + } } - } + #else + var body: Reduce { + ReducerReader { _, _ in + let _ = self.now + } + } + #endif } let store = TestStore(initialState: Feature.State(), reducer: Feature()) {