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. diff --git a/Sources/Reactor.swift b/Sources/Reactor.swift index e7bd5c1..40d62c5 100644 --- a/Sources/Reactor.swift +++ b/Sources/Reactor.swift @@ -1,23 +1,42 @@ 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 Command { + associatedtype StateType: State + func execute(state: StateType, reactor: Reactor) +} + + +// MARK: - Middlewares + public protocol AnyMiddleware { func _process(event: Event, state: Any) } 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) } } @@ -27,48 +46,40 @@ public struct Middlewares { private(set) var middleware: AnyMiddleware } + +// MARK: - Subscribers + public protocol AnySubscriber: class { func _update(with state: Any) } 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)? } -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 + +// MARK: - Reactor + +public class Reactor { - 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 { @@ -79,15 +90,8 @@ 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] = []) { + public init(state: StateType, middlewares: [AnyMiddleware] = []) { self.state = state self.middlewares = middlewares.map(Middlewares.init) } @@ -95,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) @@ -105,6 +109,14 @@ public class Reactor { subscriptions = subscriptions.filter { $0.subscriber !== subscriber } } + private func publishStateChange(subscriber: AnySubscriber?, selector: ((StateType) -> 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 +124,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: C) where C.StateType == StateType { + command.execute(state: state, reactor: self) } }