diff --git a/Sources/SwiftDux/Action/ActionDispatcher.swift b/Sources/SwiftDux/Action/ActionDispatcher.swift index 300ce05..3d87701 100644 --- a/Sources/SwiftDux/Action/ActionDispatcher.swift +++ b/Sources/SwiftDux/Action/ActionDispatcher.swift @@ -23,12 +23,9 @@ public protocol ActionDispatcher { /// Create a new `ActionDispatcher` that acts as a proxy for the current one. /// /// Actions can be modified by both the new proxy and the original dispatcher it was created from. - /// - Parameters - /// - modifyAction: An optional closure to modify the action before it continues up stream. - /// - sentAction: Called directly after an action was sent up stream. + /// - Parameter modifyAction: An optional closure to modify the action before it continues up stream. /// - Returns: a new action dispatcher. - func proxy(modifyAction: ActionModifier?, sentAction: ((Action) -> Void)?) -> ActionDispatcher - + func proxy(modifyAction: ActionModifier?) -> ActionDispatcher } extension ActionDispatcher { @@ -38,7 +35,7 @@ extension ActionDispatcher { public func callAsFunction(_ action: Action) { send(action) } - + /// Send an action that returns a cancellable object. /// - Parameter action: The action /// - Returns: A cancellable to cancel the action. diff --git a/Sources/SwiftDux/Action/ModifiedAction.swift b/Sources/SwiftDux/Action/ModifiedAction.swift deleted file mode 100644 index 2ab0951..0000000 --- a/Sources/SwiftDux/Action/ModifiedAction.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Combine -import Foundation - -/// Used internally to wrap modified actions. This allows the store to publish changes in the correct order that actions were sent. -internal struct ModifiedAction: Action { - var action: Action - var previousActions: [Action] - - init(action: Action, previousActions: [Action] = []) { - self.action = action - self.previousActions = previousActions - } - - init(action: Action, previousAction: Action) { - self.action = action - if let previousAction = previousAction as? ModifiedAction { - self.previousActions = previousAction.previousActions + [previousAction.action] - } else { - self.previousActions = [previousAction] - } - } - - func modified(with newAction: Action) -> ModifiedAction { - ModifiedAction( - action: newAction, - previousActions: previousActions + [action] - ) - } -} diff --git a/Sources/SwiftDux/Store/Store.swift b/Sources/SwiftDux/Store/Store.swift index fdb3343..4c57aa1 100644 --- a/Sources/SwiftDux/Store/Store.swift +++ b/Sources/SwiftDux/Store/Store.swift @@ -48,23 +48,16 @@ public final class Store where State: StateType { extension Store: ActionDispatcher { - // swift-format-disable: UseLetInEveryBoundCaseVariable - /// Sends an action to the store to mutate its state. /// - Parameter action: The action to mutate the state. public func send(_ action: Action) { - switch action { - case let action as ActionPlan: + if let action = action as? ActionPlan { send(actionPlan: action) - case let modifiedAction as ModifiedAction: - send(modifiedAction: modifiedAction) - default: + } else { reduceAction(action) } } - // swift-format-enable: UseLetInEveryBoundCaseVariable - /// Handles the sending of normal action plans. private func send(actionPlan: ActionPlan) { var cancellable: AnyCancellable? = nil @@ -81,23 +74,14 @@ extension Store: ActionDispatcher { didChangeSubject.send(actionPlan) } - private func send(modifiedAction: ModifiedAction) { - send(modifiedAction.action) - modifiedAction.previousActions.forEach { self.didChangeSubject.send($0) } - } - /// Create a new `ActionDispatcher` that acts as a proxy between the action sender and the store. It optionally allows actions to be /// modified or tracked. - /// - Parameters - /// - modifyAction: An optional closure to modify the action before it continues up stream. - /// - sentAction: Called directly after an action was sent up stream. + /// - Parameter modifyAction: An optional closure to modify the action before it continues up stream. /// - Returns: a new action dispatcher. - public func proxy(modifyAction: ActionModifier? = nil, sentAction: ((Action) -> Void)? = nil) -> ActionDispatcher { - StoreActionDispatcher( - upstream: self, - modifyAction: modifyAction, - sentAction: sentAction + public func proxy(modifyAction: ActionModifier? = nil) -> ActionDispatcher { + StoreProxy( + store: self, + modifyAction: modifyAction ) } - } diff --git a/Sources/SwiftDux/Store/StoreActionDispatcher.swift b/Sources/SwiftDux/Store/StoreActionDispatcher.swift deleted file mode 100644 index 0992515..0000000 --- a/Sources/SwiftDux/Store/StoreActionDispatcher.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Combine -import Foundation - -/// A dispatcher tied to an upstream `Store<_>` object. This is useful to proxy dispatched actions. -/// -/// Use the `Store<_>.proxy(modifyAction:)` or the `StoreActionDispatcher<_>.proxy(modifyAction:)` -/// methods to create a new `StoreActionDispatcher`. -/// -/// ``` -/// struct ParentView : View { -/// -/// var body: some View { -/// ChildView() -/// .modifyActions(self.routeChildActions) -/// } -/// -/// func routeChildActions(action: Action) -> Action? { -/// if let action = $0 as? ChildAction { -/// return ParentAction.routeChildAction(action, forId: parentId) -/// } -/// return action // Send original action. -/// } -/// } -/// ``` -internal final class StoreActionDispatcher: ActionDispatcher where State: StateType { - - private let upstream: Store - private let modifyAction: ActionModifier? - private let sentAction: ((Action) -> Void)? - - /// Creates a new `StoreActionDispatcher` for the upstream store. - /// - Parameters - /// - upstream: The store object. - /// - modifyAction: Modifies a dispatched action before sending it off to the upstream store. - /// - sentAction: Callback fired when an action was sent by this dispatcher. - init(upstream: Store, modifyAction: ActionModifier? = nil, sentAction: ((Action) -> Void)? = nil) { - self.upstream = upstream - self.modifyAction = modifyAction - self.sentAction = sentAction - } - - /// Sends an action to a reducer to mutate the state of the application. - /// - Parameter action: An action to dispatch to the store. - func send(_ action: Action) { - if let action = action as? ActionPlan { - send(actionPlan: action) - } else { - if let newAction = modifyAction?(action) { - upstream.send(ModifiedAction(action: newAction, previousAction: action)) - } else { - upstream.send(action) - } - sentAction?(action) - } - } - - /// Sends a self-contained action plan to mutate the application's state. Action plans are typically - /// used when multiple actions must be dispatched or there's asynchronous actions that must be - /// performed. - /// - /// The dispatching of actions should always be done on the main thread. Action plans can be used - /// to offload to other threads to perform complex workflows before pushing the changes into the state - /// on the main thread. - /// - Parameter actionPlan: The action to dispatch - private func send(actionPlan: ActionPlan) { - var cancellable: AnyCancellable? - let storeProxy = StoreProxy( - store: upstream, - done: { - cancellable?.cancel() - cancellable = nil - } - ) - cancellable = actionPlan.run(storeProxy) { [storeProxy] in - storeProxy.done() - } - upstream.didChangeSubject.send(actionPlan) - sentAction?(actionPlan) - } - -} - -extension StoreActionDispatcher { - - /// Create a new `ActionDispatcher` that acts as a proxy of the current one. Actions will be modified - /// by both the new proxy and the original dispatcher it was created from. - /// - Parameters - /// - modifyAction: An optional closure to modify the action before it continues up stream. - /// - sentAction: Called directly after an action was sent up stream. - /// - Returns: a new action dispatcher. - func proxy(modifyAction: ActionModifier? = nil, sentAction: ((Action) -> Void)? = nil) -> ActionDispatcher { - StoreActionDispatcher( - upstream: self.upstream, - modifyAction: { [weak self] (action: Action) -> Action? in - var modifiedAction: Action? - if let modifyAction = modifyAction { - modifiedAction = modifyAction(action) - } - if let nextModifyAction = self?.modifyAction { - modifiedAction = nextModifyAction(modifiedAction ?? action) - } - return modifiedAction - }, - sentAction: sentAction - ) - } - -} diff --git a/Sources/SwiftDux/Store/StoreProxy.swift b/Sources/SwiftDux/Store/StoreProxy.swift index 7f69246..c2a4f34 100644 --- a/Sources/SwiftDux/Store/StoreProxy.swift +++ b/Sources/SwiftDux/Store/StoreProxy.swift @@ -12,6 +12,9 @@ public struct StoreProxy: ActionDispatcher where State: StateType { /// Subscribe to state changes. private unowned var store: Store + /// Send an action to the next middleware + private var modifyAction: ActionModifier? + /// Send an action to the next middleware private var nextBlock: SendAction? @@ -27,8 +30,22 @@ public struct StoreProxy: ActionDispatcher where State: StateType { store.didChange } - internal init(store: Store, next: SendAction? = nil, done: (() -> Void)? = nil) { + internal init(store: Store, modifyAction: ActionModifier? = nil, next: SendAction? = nil, done: (() -> Void)? = nil) { self.store = store + self.modifyAction = modifyAction + self.nextBlock = next + self.doneBlock = done + } + + internal init(store: StoreProxy, modifyAction: ActionModifier? = nil, next: SendAction? = nil, done: (() -> Void)? = nil) { + self.store = store.store + self.modifyAction = modifyAction.flatMap { outer in + store.modifyAction.map { inner in + { action in + inner(action).flatMap { outer($0) } + } + } ?? outer + } self.nextBlock = next self.doneBlock = done } @@ -36,6 +53,7 @@ public struct StoreProxy: ActionDispatcher where State: StateType { /// Send an action to the store. /// - Parameter action: The action to send public func send(_ action: Action) { + let action = modifyAction.flatMap { $0(action) } ?? action store.send(action) } @@ -53,7 +71,7 @@ public struct StoreProxy: ActionDispatcher where State: StateType { doneBlock?() } - public func proxy(modifyAction: ActionModifier?, sentAction: ((Action) -> Void)?) -> ActionDispatcher { - fatalError("StoreProxy cannot create an ActionDispatcher proxy.") + public func proxy(modifyAction: ActionModifier?) -> ActionDispatcher { + StoreProxy(store: self, modifyAction: modifyAction) } } diff --git a/Sources/SwiftDux/UI/Extensions/Environment+ActionDispatcher.swift b/Sources/SwiftDux/UI/Extensions/Environment+ActionDispatcher.swift index 85b4b3e..691e340 100644 --- a/Sources/SwiftDux/UI/Extensions/Environment+ActionDispatcher.swift +++ b/Sources/SwiftDux/UI/Extensions/Environment+ActionDispatcher.swift @@ -8,7 +8,7 @@ internal struct NoopActionDispatcher: ActionDispatcher { print("Tried dispatching an action `\(action)` without providing a store object.") } - func proxy(modifyAction: ActionModifier?, sentAction: ((Action) -> Void)?) -> ActionDispatcher { + func proxy(modifyAction: ActionModifier? = nil) -> ActionDispatcher { print("Tried proxying an action dispatcher before providing a store object.") return self } diff --git a/Sources/SwiftDux/UI/ViewModifiers/OnActionViewModifier.swift b/Sources/SwiftDux/UI/ViewModifiers/OnActionViewModifier.swift index 0ea68f1..cb9d7e9 100644 --- a/Sources/SwiftDux/UI/ViewModifiers/OnActionViewModifier.swift +++ b/Sources/SwiftDux/UI/ViewModifiers/OnActionViewModifier.swift @@ -10,8 +10,11 @@ internal struct OnActionViewModifier: ViewModifier { } public func body(content: Content) -> some View { - let proxy = actionDispatcher.proxy(modifyAction: perform, sentAction: nil) - return content.environment(\.actionDispatcher, proxy) + var nextActionDispatcher = actionDispatcher + if let perform = perform { + nextActionDispatcher = actionDispatcher.proxy(modifyAction: perform) + } + return content.environment(\.actionDispatcher, nextActionDispatcher) } } diff --git a/Sources/SwiftDux/UI/ViewModifiers/StoreProviderViewModifier.swift b/Sources/SwiftDux/UI/ViewModifiers/StoreProviderViewModifier.swift index 40c114f..236dca6 100644 --- a/Sources/SwiftDux/UI/ViewModifiers/StoreProviderViewModifier.swift +++ b/Sources/SwiftDux/UI/ViewModifiers/StoreProviderViewModifier.swift @@ -5,7 +5,6 @@ import SwiftUI internal struct StoreProviderViewModifier: ViewModifier where State: StateType { private var store: Store private var connection: StateConnection - private var actionDispatcher: ActionDispatcher internal init(store: Store) { self.store = store @@ -17,13 +16,12 @@ internal struct StoreProviderViewModifier: ViewModifier where State: Stat changePublisher: store.didChange, emitChanges: false ) - self.actionDispatcher = store.proxy() } public func body(content: Content) -> some View { content .environmentObject(connection) - .environment(\.actionDispatcher, actionDispatcher) + .environment(\.actionDispatcher, store) .environment(\.storeUpdated, store.didChange) } diff --git a/Tests/SwiftDuxTests/Action/ActionPlanTests.swift b/Tests/SwiftDuxTests/Action/ActionPlanTests.swift index 7b2b9d9..4458e12 100644 --- a/Tests/SwiftDuxTests/Action/ActionPlanTests.swift +++ b/Tests/SwiftDuxTests/Action/ActionPlanTests.swift @@ -81,7 +81,7 @@ final class ActionPlanTests: XCTestCase { } } - let cancellable = actionPlan.sendAsCancellable(store) + let cancellable = store.sendAsCancellable(actionPlan) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { cancellable.cancel() diff --git a/Tests/SwiftDuxTests/Action/ModifiedActionTests.swift b/Tests/SwiftDuxTests/Action/ModifiedActionTests.swift deleted file mode 100644 index 884a4bc..0000000 --- a/Tests/SwiftDuxTests/Action/ModifiedActionTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -import XCTest -import Combine -import Dispatch -@testable import SwiftDux - -final class ModifiedActionTests: XCTestCase { - - override func setUp() { - } - - func testCreateModifiedAction() { - let modifiedAction = ModifiedAction(action: TestAction.actionA) - XCTAssertTrue(modifiedAction.action as? TestAction == TestAction.actionA) - } - - func testCreateModifiedActionWithPreviousAction() { - let modifiedActionA = ModifiedAction(action: TestAction.actionA) - let modifiedActionB = modifiedActionA.modified(with: TestAction.actionB) - XCTAssertTrue(modifiedActionB.action as? TestAction == TestAction.actionB) - XCTAssertTrue(modifiedActionB.previousActions[0] as? TestAction == TestAction.actionA) - } - -} - -extension ModifiedActionTests { - - enum TestAction: Action, Equatable { - case actionA - case actionB - } - -} diff --git a/Tests/SwiftDuxTests/StoreActionDispatcherTests.swift b/Tests/SwiftDuxTests/StoreActionDispatcherTests.swift index 0106ad9..ad44ccd 100644 --- a/Tests/SwiftDuxTests/StoreActionDispatcherTests.swift +++ b/Tests/SwiftDuxTests/StoreActionDispatcherTests.swift @@ -6,20 +6,20 @@ final class StoreActionDispatcherTests: XCTestCase { func testBasicActionDispatchingValue() { let store = Store(state: TestState.defaultState, reducer: TestReducer()) - let dispatcher = store.proxy() - dispatcher.send(TodoListAction.addTodo(toList: "123", withText: "My Todo")) + let dispatch = store.proxy() + dispatch(TodoListAction.addTodo(toList: "123", withText: "My Todo")) XCTAssertEqual(store.state.todoLists["123"]?.todos.filter { $0.text == "My Todo"}.count, 1) } func testModifyingActionsValue() { let store = Store(state: TestState.defaultState, reducer: TestReducer()) - let dispatcher = store.proxy(modifyAction: { + let dispatch = store.proxy { if $0 is TodoListAction { return TestAction.routeTodoAction(forList: "123", action: $0) } return $0 - }) - dispatcher.send(TodoListAction.addTodo2(withText: "My Todo")) + } + dispatch(TodoListAction.addTodo2(withText: "My Todo")) XCTAssertEqual(store.state.todoLists["123"]?.todos.filter { $0.text == "My Todo"}.count, 1) }