From df49749b0c5f52a8647005725a03d22fa7aaf28a Mon Sep 17 00:00:00 2001 From: Jason Larsen Date: Wed, 7 Sep 2016 15:56:42 -0600 Subject: [PATCH 1/4] remove `EventEmitter` and add `Command` --- Sources/Reactor.swift | 73 ++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/Sources/Reactor.swift b/Sources/Reactor.swift index e7bd5c1..77fc665 100644 --- a/Sources/Reactor.swift +++ b/Sources/Reactor.swift @@ -1,11 +1,41 @@ import Foundation -public protocol Event {} + + +// MARK: - State public protocol State { mutating func react(to event: Event) } + +// MARK: - Events + +public protocol Event {} + + +// MARK: - Commands + +public protocol AnyCommand { + func _execute(state: Any, reactor: Any) +} + +public protocol Command: AnyCommand { + associatedtype S: State + func execute(state: S, reactor: Reactor) +} + +extension Command { + func _execute(state: Any, reactor: Any) { + if let state = state as? S, let reactor = reactor as? Reactor { + execute(state: state, reactor: reactor) + } + } +} + + +// MARK: - Middlewares + public protocol AnyMiddleware { func _process(event: Event, state: Any) } @@ -27,6 +57,9 @@ public struct Middlewares { private(set) var middleware: AnyMiddleware } + +// MARK: - Subscribers + public protocol AnySubscriber: class { func _update(with state: Any) } @@ -50,24 +83,13 @@ public struct Subscription { } + +// MARK: - Reactor + public class Reactor { - /** - An `EventEmitter` is a function that takes the state and a reference - to the reactor and optionally returns an `Event` that will be immediately - executed. An `EventEmitter` may also use its reactor reference to perform - events at a later time, for example an async callback. - */ - public typealias EventEmitter = (ReactorState, Reactor) -> Event? - - // MARK: - Properties - private var subscriptions = [Subscription]() private var middlewares = [Middlewares]() - - - // MARK: - State - private (set) var state: ReactorState { didSet { subscriptions = subscriptions.filter { $0.subscriber != nil } @@ -79,13 +101,6 @@ public class Reactor { } } - private func publishStateChange(subscriber: AnySubscriber?, selector: ((ReactorState) -> Any)?) { - if let selector = selector { - subscriber?._update(with: selector(self.state)) - } else { - subscriber?._update(with: self.state) - } - } public init(state: ReactorState, middlewares: [AnyMiddleware] = []) { self.state = state @@ -105,6 +120,14 @@ public class Reactor { subscriptions = subscriptions.filter { $0.subscriber !== subscriber } } + private func publishStateChange(subscriber: AnySubscriber?, selector: ((ReactorState) -> Any)?) { + if let selector = selector { + subscriber?._update(with: selector(self.state)) + } else { + subscriber?._update(with: self.state) + } + } + // MARK: - Events public func fire(event: Event) { @@ -112,10 +135,8 @@ public class Reactor { middlewares.forEach { $0.middleware._process(event: event, state: state) } } - public func fire(emitter: EventEmitter) { - if let event = emitter(state, self) { - fire(event: event) - } + public func fire(command: AnyCommand) { + command._execute(state: state, reactor: self) } } From 0183981bf79d2b1d3d562b4ec4bfdde33fbf8f11 Mon Sep 17 00:00:00 2001 From: Jason Larsen Date: Wed, 7 Sep 2016 16:00:18 -0600 Subject: [PATCH 2/4] remove `AnyCommand` --- Sources/Reactor.swift | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Sources/Reactor.swift b/Sources/Reactor.swift index 77fc665..630c9b2 100644 --- a/Sources/Reactor.swift +++ b/Sources/Reactor.swift @@ -16,23 +16,12 @@ public protocol Event {} // MARK: - Commands -public protocol AnyCommand { - func _execute(state: Any, reactor: Any) -} -public protocol Command: AnyCommand { +public protocol Command { associatedtype S: State func execute(state: S, reactor: Reactor) } -extension Command { - func _execute(state: Any, reactor: Any) { - if let state = state as? S, let reactor = reactor as? Reactor { - execute(state: state, reactor: reactor) - } - } -} - // MARK: - Middlewares @@ -135,8 +124,8 @@ public class Reactor { middlewares.forEach { $0.middleware._process(event: event, state: state) } } - public func fire(command: AnyCommand) { - command._execute(state: state, reactor: self) + public func fire(command: C) where C.S == ReactorState { + command.execute(state: state, reactor: self) } } From 0367fd998156f1112bad3a249adf0131969230ea Mon Sep 17 00:00:00 2001 From: Jason Larsen Date: Wed, 7 Sep 2016 16:52:23 -0600 Subject: [PATCH 3/4] rename associatedtypes and generics to StateType I find this easier on the eyes than ReactorState and/or S everywhere --- Sources/Reactor.swift | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/Reactor.swift b/Sources/Reactor.swift index 630c9b2..40d62c5 100644 --- a/Sources/Reactor.swift +++ b/Sources/Reactor.swift @@ -18,8 +18,8 @@ public protocol Event {} public protocol Command { - associatedtype S: State - func execute(state: S, reactor: Reactor) + associatedtype StateType: State + func execute(state: StateType, reactor: Reactor) } @@ -30,13 +30,13 @@ public protocol AnyMiddleware { } public protocol Middleware: AnyMiddleware { - associatedtype State - func process(event: Event, state: State) + associatedtype StateType + func process(event: Event, state: StateType) } extension Middleware { public func _process(event: Event, state: Any) { - if let state = state as? State { + if let state = state as? StateType { process(event: event, state: state) } } @@ -54,32 +54,32 @@ public protocol AnySubscriber: class { } public protocol Subscriber: AnySubscriber { - associatedtype State - func update(with state: State) + associatedtype StateType + func update(with state: StateType) } extension Subscriber { public func _update(with state: Any) { - if let state = state as? State { + if let state = state as? StateType { update(with: state) } } } -public struct Subscription { +public struct Subscription { private(set) weak var subscriber: AnySubscriber? = nil - let selector: ((ReactorState) -> Any)? + let selector: ((StateType) -> Any)? } // MARK: - Reactor -public class Reactor { +public class Reactor { - private var subscriptions = [Subscription]() - private var middlewares = [Middlewares]() - private (set) var state: ReactorState { + private var subscriptions = [Subscription]() + private var middlewares = [Middlewares]() + private (set) var state: StateType { didSet { subscriptions = subscriptions.filter { $0.subscriber != nil } DispatchQueue.main.async { @@ -91,7 +91,7 @@ public class Reactor { } - public init(state: ReactorState, middlewares: [AnyMiddleware] = []) { + public init(state: StateType, middlewares: [AnyMiddleware] = []) { self.state = state self.middlewares = middlewares.map(Middlewares.init) } @@ -99,7 +99,7 @@ public class Reactor { // MARK: - Subscriptions - public func add(subscriber: AnySubscriber, selector: ((ReactorState) -> Any)? = nil) { + public func add(subscriber: AnySubscriber, selector: ((StateType) -> Any)? = nil) { guard !subscriptions.contains(where: {$0.subscriber === subscriber}) else { return } subscriptions.append(Subscription(subscriber: subscriber, selector: selector)) publishStateChange(subscriber: subscriber, selector: selector) @@ -109,7 +109,7 @@ public class Reactor { subscriptions = subscriptions.filter { $0.subscriber !== subscriber } } - private func publishStateChange(subscriber: AnySubscriber?, selector: ((ReactorState) -> Any)?) { + private func publishStateChange(subscriber: AnySubscriber?, selector: ((StateType) -> Any)?) { if let selector = selector { subscriber?._update(with: selector(self.state)) } else { @@ -124,7 +124,7 @@ public class Reactor { middlewares.forEach { $0.middleware._process(event: event, state: state) } } - public func fire(command: C) where C.S == ReactorState { + public func fire(command: C) where C.StateType == StateType { command.execute(state: state, reactor: self) } From 1e47af37f712bca34449f78bc5133843624fafeb Mon Sep 17 00:00:00 2001 From: Jason Larsen Date: Wed, 7 Sep 2016 17:28:15 -0600 Subject: [PATCH 4/4] update README with some basic Command documentation --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 5ecdb2b..6c98f9a 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,31 @@ extension ViewController: Subscriber { By subscribing and subscribing in `viewDidAppear`/`viewDidDisappear` respectively, we ensure that whenever this view controller is visible it is up to date with the latest application state. Upon initial subscription, the reactor will send the latest state to the subscriber's `update` function. Button presses forward events back to the reactor, which will then update the state and result in subsequent calls to `update`. (note: the Reactor always dispatches back to the main thread when it updates subscribers, so it is safe to perform UI updates in `update`.) +## Commands + +Sometimes you want to fire an `Event` at a later point, for example after a network request, database query, or other asynchronous operation. In these cases, `Command` helps you interact with the `Reactor` in a safe and consistent way. + +```swift +struct CreatePlayer: Command { + var session = URLSession.shared + var player: Player + + func execute(state: RPGState, reactor: Reactor) { + let task = session.dataTask(with: player.createRequest()) { data, response, error in + // handle response appropriately + // then fire an update back to the reactor + reactor.fire(event: AddPlayer(player: player)) + } + task.resume() + } +} + +// to fire a command +reactor.fire(command: CreatePlayer(player: myNewPlayer)) +``` + +Commands get a copy of the current state, and a reference to the Reactor so they can fire Events as necessary. + ## Middleware Sometimes you want to do something with an event besides just update application state. This is where `Middleware` comes into play. When you create a `Reactor`, along with the initial state, you may pass in an array of middleware. Each middleware gets called every time an event is passed in. Middleware is not allowed to mutate the state, but it does get a copy of the state along with the event. Middleware makes it easy to add things like logging, analytics, and error handling to an application.