Skip to content

Add Commands! #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}

}