From 2aef68d7d74a241155364166be05fef6b891c01d Mon Sep 17 00:00:00 2001 From: Steven Lambion Date: Thu, 6 Feb 2020 19:13:40 -0800 Subject: [PATCH 1/7] Removed onAppearAsync and onDisappearAsync --- .../UI/Extensions/View+AppearanceEvents.swift | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 Sources/SwiftDux/UI/Extensions/View+AppearanceEvents.swift diff --git a/Sources/SwiftDux/UI/Extensions/View+AppearanceEvents.swift b/Sources/SwiftDux/UI/Extensions/View+AppearanceEvents.swift deleted file mode 100644 index e5d959b..0000000 --- a/Sources/SwiftDux/UI/Extensions/View+AppearanceEvents.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Dispatch -import SwiftUI - -extension View { - - /// Performs an action asynchronously on the main thread when a view appears. - /// - /// SwiftUI doesn't update a view if changes are performed in the onAppear method. - /// This includes changes to the application state from dispatched actions. This method - /// allows actions to be dispatched when the view appears. - /// - Parameter perform: The action to run asynchronously - /// - Returns: The modified view. - @available(*, deprecated) - public func onAppearAsync(perform: @escaping () -> Void) -> some View { - onAppear { - DispatchQueue.main.async(execute: perform) - } - } - - /// Performs an action asynchronously on the main thread when a view appears. - /// - /// - Parameter perform: The action to run asynchronously - /// - Returns: The modified view. - @available(*, deprecated) - public func onDisappearAsync(perform: @escaping () -> Void) -> some View { - onDisappear { - DispatchQueue.main.async(execute: perform) - } - } - -} From 1407d5aa218dba033cba7cc7bed2cfc6352830a3 Mon Sep 17 00:00:00 2001 From: Steven Lambion Date: Fri, 7 Feb 2020 06:59:01 -0800 Subject: [PATCH 2/7] Added callAsFunction to ActionDispatcher. --- .../SwiftDux/Action/ActionDispatcher.swift | 12 +++++++-- Sources/SwiftDux/Action/ActionPlan.swift | 2 +- .../SwiftDux/Action/ActionSubscriber.swift | 19 ++++--------- .../SwiftDux/Action/CancellableAction.swift | 2 +- Sources/SwiftDux/Store/Store.swift | 27 ------------------- Sources/SwiftDux/Store/StoreProxy.swift | 6 ++++- Sources/SwiftDux/UI/MappedDispatch.swift | 4 +-- .../Action/ActionPlanTests.swift | 2 +- 8 files changed, 25 insertions(+), 49 deletions(-) diff --git a/Sources/SwiftDux/Action/ActionDispatcher.swift b/Sources/SwiftDux/Action/ActionDispatcher.swift index 9c7f1be..3c330ac 100644 --- a/Sources/SwiftDux/Action/ActionDispatcher.swift +++ b/Sources/SwiftDux/Action/ActionDispatcher.swift @@ -3,7 +3,7 @@ import Foundation /// A closure that dispatches an action. /// -/// - Parameter action: Dispatches the given state synchronously. +/// - Parameter action: Dispatches the given action synchronously. public typealias SendAction = (Action) -> Void /// A closure that can return a new action from a previous one. If no action is returned, @@ -27,6 +27,14 @@ public protocol ActionDispatcher { /// - 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?, sentAction: SendAction?) -> ActionDispatcher + func proxy(modifyAction: ActionModifier?, sentAction: ((Action) -> Void)?) -> ActionDispatcher + +} + +extension ActionDispatcher { + + public func callAsFunction(_ action: Action) { + send(action) + } } diff --git a/Sources/SwiftDux/Action/ActionPlan.swift b/Sources/SwiftDux/Action/ActionPlan.swift index e00f8ad..5b1003c 100644 --- a/Sources/SwiftDux/Action/ActionPlan.swift +++ b/Sources/SwiftDux/Action/ActionPlan.swift @@ -121,7 +121,7 @@ public struct ActionPlan: CancellableAction where State: StateType { /// /// - Parameter send: The send function that dispatches an action. /// - Returns: AnyCancellable to cancel the action plan. - public func sendAsCancellable(_ send: SendAction) -> Cancellable { + public func sendAsCancellable(_ send: ActionDispatcher) -> Cancellable { var publisherCancellable: AnyCancellable? = nil send( ActionPlan { store, completed in diff --git a/Sources/SwiftDux/Action/ActionSubscriber.swift b/Sources/SwiftDux/Action/ActionSubscriber.swift index a686bb5..c78a272 100644 --- a/Sources/SwiftDux/Action/ActionSubscriber.swift +++ b/Sources/SwiftDux/Action/ActionSubscriber.swift @@ -6,7 +6,7 @@ final public class ActionSubscriber: Subscriber { public typealias ReceivedCompletion = () -> Void - private let sendAction: SendAction + private let actionDispatcher: ActionDispatcher private let receivedCompletion: ReceivedCompletion? private var subscription: Subscription? = nil { willSet { @@ -15,8 +15,8 @@ final public class ActionSubscriber: Subscriber { } } - internal init(sendAction: @escaping SendAction, receivedCompletion: ReceivedCompletion?) { - self.sendAction = sendAction + internal init(actionDispatcher: ActionDispatcher, receivedCompletion: ReceivedCompletion?) { + self.actionDispatcher = actionDispatcher self.receivedCompletion = receivedCompletion } @@ -26,7 +26,7 @@ final public class ActionSubscriber: Subscriber { } public func receive(_ input: Action) -> Subscribers.Demand { - sendAction(input) + actionDispatcher(input) return .max(1) } @@ -49,17 +49,8 @@ extension Publisher where Output == Action, Failure == Never { /// - receivedCompletion: An optional block called when the publisher completes. /// - Returns: A cancellable to unsubscribe. public func send(to actionDispatcher: ActionDispatcher, receivedCompletion: ActionSubscriber.ReceivedCompletion? = nil) -> AnyCancellable { - self.send(to: actionDispatcher.send, receivedCompletion: receivedCompletion) - } - - /// Subscribe to a publisher of actions, and send the results to an action dispatcher. - /// - Parameters: - /// - sendAction: A block that dispatches actions.. - /// - receivedCompletion: An optional block called when the publisher completes. - /// - Returns: A cancellable to unsubscribe. - public func send(to sendAction: @escaping SendAction, receivedCompletion: ActionSubscriber.ReceivedCompletion? = nil) -> AnyCancellable { let subscriber = ActionSubscriber( - sendAction: sendAction, + actionDispatcher: actionDispatcher, receivedCompletion: receivedCompletion ) self.subscribe(subscriber) diff --git a/Sources/SwiftDux/Action/CancellableAction.swift b/Sources/SwiftDux/Action/CancellableAction.swift index d9c67af..109087a 100644 --- a/Sources/SwiftDux/Action/CancellableAction.swift +++ b/Sources/SwiftDux/Action/CancellableAction.swift @@ -38,6 +38,6 @@ public protocol CancellableAction: Action { /// /// - Parameter send: The send function that dispatches an action. /// - Returns: AnyCancellable to cancel the action plan. - func sendAsCancellable(_ send: SendAction) -> Cancellable + func sendAsCancellable(_ send: ActionDispatcher) -> Cancellable } diff --git a/Sources/SwiftDux/Store/Store.swift b/Sources/SwiftDux/Store/Store.swift index e404967..fdb3343 100644 --- a/Sources/SwiftDux/Store/Store.swift +++ b/Sources/SwiftDux/Store/Store.swift @@ -101,30 +101,3 @@ extension Store: ActionDispatcher { } } - -extension Publisher where Output == Action, Failure == Never { - - /// Subscribe to a publisher of actions, and send the results to a store. - /// - Parameters: - /// - store: A store proxy - /// - receivedCompletion: An optional block called when the publisher completes. - /// - Returns: A cancellable to unsubscribe. - public func send(to store: Store, receivedCompletion: ActionSubscriber.ReceivedCompletion? = nil) -> AnyCancellable - where State: StateType { - self.send(to: store, receivedCompletion: receivedCompletion) - } - - /// Subscribe to a publisher of actions, and send the results to a store. - /// - Parameters: - /// - store: A store proxy - /// - receivedCompletion: An optional block called when the publisher completes. - /// - Returns: A cancellable to unsubscribe. - public func send(to store: StoreProxy, receivedCompletion: ActionSubscriber.ReceivedCompletion? = nil) -> AnyCancellable - where State: StateType { - let cancellable = self.send(to: store.send, receivedCompletion: receivedCompletion) - return AnyCancellable { [cancellable] in - cancellable.cancel() - store.done() - } - } -} diff --git a/Sources/SwiftDux/Store/StoreProxy.swift b/Sources/SwiftDux/Store/StoreProxy.swift index 987d882..7f69246 100644 --- a/Sources/SwiftDux/Store/StoreProxy.swift +++ b/Sources/SwiftDux/Store/StoreProxy.swift @@ -7,7 +7,7 @@ import Foundation /// continue to the next middleware, or subscribe to store changes. With the proxy, /// middleware don't have to worry about retaining the store. Instead, the proxy provides /// a safe API to access a weak reference to it. -public struct StoreProxy where State: StateType { +public struct StoreProxy: ActionDispatcher where State: StateType { /// Subscribe to state changes. private unowned var store: Store @@ -52,4 +52,8 @@ public struct StoreProxy where State: StateType { public func done() { doneBlock?() } + + public func proxy(modifyAction: ActionModifier?, sentAction: ((Action) -> Void)?) -> ActionDispatcher { + fatalError("StoreProxy cannot create an ActionDispatcher proxy.") + } } diff --git a/Sources/SwiftDux/UI/MappedDispatch.swift b/Sources/SwiftDux/UI/MappedDispatch.swift index b229390..886734e 100644 --- a/Sources/SwiftDux/UI/MappedDispatch.swift +++ b/Sources/SwiftDux/UI/MappedDispatch.swift @@ -20,8 +20,8 @@ public struct MappedDispatch: DynamicProperty { @Environment(\.actionDispatcher) private var actionDispatcher: ActionDispatcher - public var wrappedValue: SendAction { - actionDispatcher.send + public var wrappedValue: ActionDispatcher { + actionDispatcher } public init() {} diff --git a/Tests/SwiftDuxTests/Action/ActionPlanTests.swift b/Tests/SwiftDuxTests/Action/ActionPlanTests.swift index 3406b41..7b2b9d9 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.send) + let cancellable = actionPlan.sendAsCancellable(store) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { cancellable.cancel() From 02c27bcf9cc07015575021688c8f7b9bbfd80ce1 Mon Sep 17 00:00:00 2001 From: Steven Lambion Date: Fri, 7 Feb 2020 07:20:45 -0800 Subject: [PATCH 3/7] Try to validate only master and PRs. --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42e04a7..291fcfa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,8 @@ -on: [push, pull_request] +on: + - push: + - 'master' + - pull_request: + - '**' name: build jobs: validate: From f02ee1e2a57d5e99f6c8594ea24bb3cd3694545e Mon Sep 17 00:00:00 2001 From: Steven Lambion Date: Fri, 7 Feb 2020 07:23:18 -0800 Subject: [PATCH 4/7] Removed unintended array. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 291fcfa..b695b80 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ on: - - push: + push: - 'master' - - pull_request: + pull_request: - '**' name: build jobs: From 9b4848fdbce0064e440392815f2287c21e3e536a Mon Sep 17 00:00:00 2001 From: Steven Lambion Date: Fri, 7 Feb 2020 07:26:14 -0800 Subject: [PATCH 5/7] Added branches --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b695b80..11e80ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,10 @@ on: push: - - 'master' + branches: + - 'master' pull_request: - - '**' + branches: + - '**' name: build jobs: validate: From 46d933d147843a875bdbfba5972ec95d93777188 Mon Sep 17 00:00:00 2001 From: Steven Lambion Date: Fri, 7 Feb 2020 10:52:17 -0800 Subject: [PATCH 6/7] Send cancellable actions from ActionDispatcher. --- Sources/SwiftDux/Action/ActionDispatcher.swift | 10 +++++++++- Sources/SwiftDux/Action/ActionPlan.swift | 8 ++------ Sources/SwiftDux/Action/CancellableAction.swift | 3 +-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftDux/Action/ActionDispatcher.swift b/Sources/SwiftDux/Action/ActionDispatcher.swift index 3c330ac..300ce05 100644 --- a/Sources/SwiftDux/Action/ActionDispatcher.swift +++ b/Sources/SwiftDux/Action/ActionDispatcher.swift @@ -33,8 +33,16 @@ public protocol ActionDispatcher { extension ActionDispatcher { + /// Sends an action to a reducer to mutate the state of the application. + /// - Parameter action: An action to dispatch to the store. 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. + public func sendAsCancellable(_ action: CancellableAction) -> AnyCancellable { + action.sendAsCancellable(self) + } } diff --git a/Sources/SwiftDux/Action/ActionPlan.swift b/Sources/SwiftDux/Action/ActionPlan.swift index 5b1003c..94b6a4e 100644 --- a/Sources/SwiftDux/Action/ActionPlan.swift +++ b/Sources/SwiftDux/Action/ActionPlan.swift @@ -121,7 +121,7 @@ public struct ActionPlan: CancellableAction where State: StateType { /// /// - Parameter send: The send function that dispatches an action. /// - Returns: AnyCancellable to cancel the action plan. - public func sendAsCancellable(_ send: ActionDispatcher) -> Cancellable { + public func sendAsCancellable(_ send: ActionDispatcher) -> AnyCancellable { var publisherCancellable: AnyCancellable? = nil send( ActionPlan { store, completed in @@ -132,11 +132,7 @@ public struct ActionPlan: CancellableAction where State: StateType { return publisherCancellable } ) - - return AnyCancellable { - publisherCancellable?.cancel() - publisherCancellable = nil - } + return AnyCancellable { [publisherCancellable] in publisherCancellable?.cancel() } } /// Dispatches another action plan after this one has completed. This allows diff --git a/Sources/SwiftDux/Action/CancellableAction.swift b/Sources/SwiftDux/Action/CancellableAction.swift index 109087a..8451846 100644 --- a/Sources/SwiftDux/Action/CancellableAction.swift +++ b/Sources/SwiftDux/Action/CancellableAction.swift @@ -38,6 +38,5 @@ public protocol CancellableAction: Action { /// /// - Parameter send: The send function that dispatches an action. /// - Returns: AnyCancellable to cancel the action plan. - func sendAsCancellable(_ send: ActionDispatcher) -> Cancellable - + func sendAsCancellable(_ send: ActionDispatcher) -> AnyCancellable } From ed22980af10ad2cac7d2626515801fe4e010db92 Mon Sep 17 00:00:00 2001 From: Steven Lambion Date: Fri, 7 Feb 2020 16:38:58 -0800 Subject: [PATCH 7/7] Simplified action dispatcher proxy API --- .../SwiftDux/Action/ActionDispatcher.swift | 9 +- Sources/SwiftDux/Action/ModifiedAction.swift | 29 ----- Sources/SwiftDux/Store/Store.swift | 30 ++--- .../Store/StoreActionDispatcher.swift | 108 ------------------ Sources/SwiftDux/Store/StoreProxy.swift | 24 +++- .../Environment+ActionDispatcher.swift | 2 +- .../ViewModifiers/OnActionViewModifier.swift | 7 +- .../StoreProviderViewModifier.swift | 4 +- .../Action/ActionPlanTests.swift | 2 +- .../Action/ModifiedActionTests.swift | 32 ------ .../StoreActionDispatcherTests.swift | 10 +- 11 files changed, 44 insertions(+), 213 deletions(-) delete mode 100644 Sources/SwiftDux/Action/ModifiedAction.swift delete mode 100644 Sources/SwiftDux/Store/StoreActionDispatcher.swift delete mode 100644 Tests/SwiftDuxTests/Action/ModifiedActionTests.swift 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) }