Last active
July 20, 2017 04:55
-
-
Save levi/2d6e899df8387f88ba2a549553a595d0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: Implementation of a redux-like pattern in swift. Its current form lacks subscriptions. | |
import UIKit | |
/*: | |
## The Message | |
A message sent to the store's reducer to perform internal state mutation. | |
This is a protocol to provide significant flexibility in the types of Messages the app layer can provide the store. The most common case is an enum type with associated values. | |
*/ | |
protocol Message {} | |
/*: | |
## The Command | |
Represents a side-effect created by the State handling an action. | |
The most common use case is firing asynchronous events. | |
The benefit of this pattern is that the State has an opportunity to update itself and perform an asynchronous action without imperative escapehatches. When all UI state is define by the app's store, the code path to updating the UI upon any action should be synchronous in most actions, such that there's no room for delay between event and UI update. Separating commands into a separate action type allow for the state's dispatch implementaiton | |
*/ | |
protocol Command { | |
func interpret(_ callback: @escaping (Message) -> ()) | |
} | |
/*: | |
## The State | |
Represents all application state. | |
Using a struct to conform to this protocol is ideal, as it utilizes Swift's copy-on-write semantics for value types — confining mutation behavior to the reducer alone. Moreover, the use of computed property extensions allow for extremely flexible transformations. | |
*/ | |
protocol StateType {} | |
/// Primative type represening the general case of handling a message through sending | |
typealias SendFunction = (Message) -> () | |
/// Message middleware to perform side effects before messages are dispatched | |
typealias Middleware<State> = (@escaping SendFunction, @escaping () -> State?) -> (@escaping SendFunction) -> SendFunction | |
/*: | |
## The Reducer | |
Handles synchronous updating of the passed state. Side-effects and asynchronous actions are returned as a series of Commands. | |
*/ | |
typealias Reducer<State: StateType> = (State, Message) -> (state: State, commands: [Command]?) | |
/*: | |
Applications have a single store that contain all application state. | |
The store intentially performs all its work on main, as it subscribes are all objects in the view hierarchy. | |
*/ | |
class Store<State: StateType> { | |
private(set) var isSending = false | |
private(set) var state: State { | |
didSet { | |
/// Update subscribers | |
} | |
} | |
private var reducer: Reducer<State> | |
/// Main invocation function for message sending. Overrideable for middleware nesting. | |
private var sendFunction: SendFunction | |
// TODO: Subscribers | |
public init(initialState: State, reducer: @escaping Reducer<State>, middleware: [Middleware<State>] = []) { | |
self.state = initialState | |
self.reducer = reducer | |
/// Register send middleware | |
self.sendFunction = middleware.reversed().reduce({ [unowned self] message in | |
return self._coreSend(message: message) | |
}, { sendFunction, middleware in | |
let send = { [weak self] in self?.sendFunction } | |
let getState = { [weak self] in self?.state } | |
return middleware(send, getState)(sendFunction) | |
}) | |
} | |
/// Sends a message to the store. Must be called on the main thread | |
/// - parameter message: Message passed to the store's reducer | |
func send(message: Message) { | |
sendFunction(message) | |
} | |
/// Performs a send synchronized on main. | |
/// - parameter message: Message passed to the store's reducer | |
func asyncSend(message: Message) { | |
DispatchQueue.main.async { | |
self.sendFunction(message) | |
} | |
} | |
/// Core sending behavior | |
/// | |
/// Recieves message and passes it into the reducer, mutating the store's state and notifying changes in one pass. | |
/// | |
/// Reducer side-effect commands are process after state mutation, enabling store subscribers to have a chance to | |
/// update before | |
private func _coreSend(message: Message) { | |
if isSending { | |
fatalError("Message sent while the state is being reduced") | |
} | |
isSending = true | |
let result = reducer(state, message) | |
// Apply the latest state to notify subscribers in one pass | |
state = result.state | |
isSending = false | |
// Respond to the reducer's side-effects | |
for command in result.commands { | |
command.interpret(self.asyncSend) | |
} | |
} | |
} | |
protocol AnySubscriber: class { | |
func _newState(state: Any) | |
} | |
protocol Subscriber { | |
associatedtype SubscriberState | |
func newState(state: SubscriberState) | |
} | |
protocol Subscriber: AnySubscriber { | |
func _newState(state: Any) { | |
if let castedState = state as? SubscriberState { | |
newState(state: castedState) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment