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 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..00f65c5c6ed2 --- /dev/null +++ b/Tests/ComposableArchitectureTests/ReducerReaderTests.swift @@ -0,0 +1,36 @@ +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 + + #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()) { + $0.date.now = Date(timeIntervalSince1970: 1234567890) + } + await store.send(.tap) + } +}