Created
June 22, 2017 13:48
-
-
Save milch/c76035b917407f74b0301b660f13b502 to your computer and use it in GitHub Desktop.
Simple, type-safe, redux-like implementation in Swift
This file contains 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
// For use in a playground | |
import UIKit | |
import XCPlayground | |
// Needed because dispatch/subscribe uses GCD for thread-safety, so everything is done in a background queue | |
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true | |
struct CookieJar { | |
let numberOfCookies : UInt | |
} | |
enum CookieAction { | |
case takeCookie | |
case putCookie | |
} | |
func appStateReducer(state : CookieJar, action : CookieAction) -> CookieJar { | |
switch action { | |
case .takeCookie: | |
return CookieJar(numberOfCookies: state.numberOfCookies > 0 ? state.numberOfCookies - 1 : 0) | |
case .putCookie: | |
return CookieJar(numberOfCookies: state.numberOfCookies + 1) | |
} | |
} | |
let jar = Store(reducer: appStateReducer, initialState: CookieJar(numberOfCookies: 5), middlewares: logger) | |
jar.subscribe { jar in | |
print("Current number of cookies:\(jar.numberOfCookies)") | |
} | |
jar.dispatch(.takeCookie) | |
jar.dispatch(.takeCookie) | |
jar.dispatch(.putCookie) | |
jar.dispatch(.putCookie) | |
jar.dispatch(.putCookie) | |
jar.dispatch(.putCookie) |
This file contains 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
// JSON.swift | |
// | |
// Created by Manu Wallner on 07.04.2017. | |
// Copyright © 2017 Manu Wallner. All rights reserved. | |
import Foundation | |
private func formatType<T>(_ value : T) -> String { | |
if value is Double || value is Float || value is Int || value is UInt { | |
return String(describing: value) | |
} else { | |
return "\"\(String(describing: value))\"" | |
} | |
} | |
private func _jsonify<T>(data : T, indentString : String) -> String { | |
let mirror = Mirror(reflecting: data) | |
if mirror.children.count == 0 { | |
return formatType(data) | |
} | |
let nextIndent = indentString + " " | |
let childrenHaveLabels = (mirror.children.first?.label) != nil | |
if childrenHaveLabels { // Write out a dictionary | |
let prettyChildren = mirror.children.map { "\(nextIndent)\"\($0.label!)\": " + _jsonify(data: $0.value, indentString: nextIndent) } | |
return "{\n" + prettyChildren.joined(separator: ",\n") + "\n\(indentString)}" | |
} else { // Write out an Array | |
let prettyChildren = mirror.children.map { nextIndent + _jsonify(data: $0.value, indentString: nextIndent) } | |
return "[\n" + prettyChildren.joined(separator: ",\n") + "\n\(indentString)]" | |
} | |
} | |
// Super simple function that easily turns simple structs to readable JSON for *debugging* | |
// Use real-people JSON for production | |
public func jsonify<T>(data : T) -> String { | |
return _jsonify(data: data, indentString: "") | |
} |
This file contains 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
// Middleware.swift | |
// | |
// Created by Manu Wallner on 08.04.2017. | |
// Copyright © 2017 XForge Software Development. All rights reserved. | |
import Foundation | |
public func logger<_State, _ActionType>(store : Store<_State, _ActionType>) -> Store<_State, _ActionType>.MiddlewareReturn { | |
return { next in | |
return { action in | |
print("Dispatching: \(action)") | |
next(action) | |
print("New state: " + jsonify(data: store.state())) | |
} | |
} | |
} |
This file contains 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
// Reducer.swift | |
// | |
// Created by Manu Wallner on 06.04.2017. | |
// Copyright © 2017 XForge Software Development. All rights reserved. | |
import Foundation | |
protocol State { } | |
// This would be more awesome if we could have separate ActionTypes so that only the right one of the combined reducers gets called ... oh well | |
public func combineReducers<_StateType, _ActionType>(_ reducers : ((_StateType, _ActionType) -> _StateType)...) -> ((_StateType, _ActionType) -> _StateType) { | |
return { (state : _StateType, action : _ActionType) in | |
var intermediateState = state | |
for reducer in reducers { | |
intermediateState = reducer(intermediateState, action) | |
} | |
return intermediateState | |
} | |
} |
This file contains 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
// Store.swift | |
// | |
// Created by Manu Wallner on 06.04.2017. | |
// Copyright © 2017 Manu Wallner. All rights reserved. | |
import Foundation | |
import Dispatch | |
public class Store<_State, _ActionType> { | |
public typealias Subscriber = (_State) -> Void | |
public typealias Reducer = (_State, _ActionType) -> _State | |
public typealias DispatchSignature = (_ActionType) -> Void | |
public typealias Middleware = ((Store) -> (@escaping DispatchSignature) -> (_ActionType) -> Void) | |
public typealias MiddlewareReturn = ((@escaping DispatchSignature) -> (_ActionType) -> Void) | |
private var subscribers = [UUID : Subscriber]() | |
private let backingQueue = DispatchQueue(label: "at.xforge.store-backing-queue", qos: .default, attributes: .concurrent) | |
private var _state : _State { | |
didSet { | |
let state = _state | |
backingQueue.sync(flags: .barrier) { [unowned self] in | |
for (_, subscriber) in self.subscribers { | |
DispatchQueue.main.async { subscriber(state) } | |
} | |
} | |
} | |
} | |
private var _dispatch : DispatchSignature! | |
private let reducer : Reducer | |
public init(reducer : @escaping Reducer, initialState : _State, middlewares: [Middleware] = []) { | |
self.reducer = reducer | |
_state = initialState | |
applyMiddleware(middlewares: middlewares) | |
} | |
public convenience init(reducer : @escaping Reducer, initialState : _State, middlewares: Middleware...) { | |
self.init(reducer: reducer, initialState: initialState, middlewares: middlewares) | |
} | |
private func dispatchImplementation(store: Store) -> DispatchSignature { | |
return { action in | |
store._state = store.reducer(store.state(), action) | |
} | |
} | |
private func applyMiddleware(middlewares : [Middleware]) { | |
let middlewares = middlewares.reversed() | |
var dispatch = self.dispatchImplementation(store: self) | |
for middleware in middlewares { | |
dispatch = middleware(self)(dispatch) | |
} | |
_dispatch = dispatch | |
} | |
public func subscribe(callback : @escaping Subscriber) -> () -> Void { | |
let identifier = UUID() | |
backingQueue.async { [unowned self] in | |
self.subscribers[identifier] = callback | |
let state = self._state | |
DispatchQueue.main.async { callback(state) } | |
} | |
return { [unowned self] in | |
self.backingQueue.sync(flags: .barrier) { [unowned self] in | |
self.subscribers.removeValue(forKey: identifier) | |
} | |
} | |
} | |
public func dispatch(_ action : _ActionType) { | |
self._dispatch(action) | |
} | |
public func state() -> _State { | |
return _state | |
} | |
deinit { | |
// Make sure all modifications finish before the deinit takes place | |
backingQueue.sync(flags: .barrier) { } | |
} | |
} | |
public protocol DefaultInitializable { | |
init() | |
} | |
// Just a convenience so the callsite doesn't have to write Store(reducer: ..., initialState: State()) | |
public extension Store where _State : DefaultInitializable { | |
public convenience init(reducer : @escaping Reducer, middlewares: Middleware...) { | |
self.init(reducer: reducer, initialState: _State(), middlewares: middlewares) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment