Skip to content

Commit

Permalink
Merge pull request #3 from jarsen/commands
Browse files Browse the repository at this point in the history
Add Commands!
  • Loading branch information
jarsen authored Sep 7, 2016
2 parents 01c87f8 + 1e47af3 commit b7ce123
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 40 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<RPGState>) {
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.
Expand Down
90 changes: 50 additions & 40 deletions Sources/Reactor.swift
Original file line number Diff line number Diff line change
@@ -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<StateType>)
}


// 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)
}
}
Expand All @@ -27,48 +46,40 @@ public struct Middlewares<ReactorState: State> {
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<ReactorState: State> {
public struct Subscription<StateType: State> {
private(set) weak var subscriber: AnySubscriber? = nil
let selector: ((ReactorState) -> Any)?
let selector: ((StateType) -> Any)?
}


public class Reactor<ReactorState: State> {

/**
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<ReactorState>) -> Event?

// MARK: - Properties

private var subscriptions = [Subscription<ReactorState>]()
private var middlewares = [Middlewares<ReactorState>]()


// MARK: - State

// MARK: - Reactor

public class Reactor<StateType: State> {

private (set) var state: ReactorState {
private var subscriptions = [Subscription<StateType>]()
private var middlewares = [Middlewares<StateType>]()
private (set) var state: StateType {
didSet {
subscriptions = subscriptions.filter { $0.subscriber != nil }
DispatchQueue.main.async {
Expand All @@ -79,23 +90,16 @@ public class Reactor<ReactorState: State> {
}
}

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)
}


// 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)
Expand All @@ -105,17 +109,23 @@ public class Reactor<ReactorState: State> {
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) {
state.react(to: event)
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<C: Command>(command: C) where C.StateType == StateType {
command.execute(state: state, reactor: self)
}

}

0 comments on commit b7ce123

Please sign in to comment.