Skip to content

Instantly share code, notes, and snippets.

@milch
Created June 22, 2017 13:48
Show Gist options
  • Save milch/c76035b917407f74b0301b660f13b502 to your computer and use it in GitHub Desktop.
Save milch/c76035b917407f74b0301b660f13b502 to your computer and use it in GitHub Desktop.
Simple, type-safe, redux-like implementation in Swift
// 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)
// 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: "")
}
// 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()))
}
}
}
// 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
}
}
// 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