Skip to content

Instantly share code, notes, and snippets.

@levi
Last active July 20, 2017 04:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save levi/2d6e899df8387f88ba2a549553a595d0 to your computer and use it in GitHub Desktop.
Save levi/2d6e899df8387f88ba2a549553a595d0 to your computer and use it in GitHub Desktop.
//: 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