Last active
June 23, 2016 16:46
-
-
Save jon-cotton/6c2d8d8a6cab4dbefae764aaa22381b2 to your computer and use it in GitHub Desktop.
Implementation of a Redux/Flux like store with a single AppState in Swift 3.0. Makes use of some of the concurrency techniques demonstrated in the WWDC 2016 talk 'Concurrent Programming With GCD in Swift 3' https://developer.apple.com/videos/play/wwdc2016/720/
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
//: Swift 3 State Store | |
import Foundation | |
import XCPlayground | |
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true | |
protocol State { | |
var counter: Int { get } | |
} | |
protocol MutableState: State { | |
var counter: Int { get set } | |
} | |
struct AppState: MutableState { | |
var counter = 0 | |
} | |
protocol StateObserver: class { | |
var isObserving: Bool { get set } | |
var queue: DispatchQueue { get } | |
func _stateDidChange(_ state: State) | |
func stateDidChange(_ state: State) | |
func startObserving() | |
func finishObserving() | |
} | |
extension StateObserver { | |
var queue: DispatchQueue { | |
return DispatchQueue.main | |
} | |
func _stateDidChange(_ state: State) { | |
if isObserving { | |
stateDidChange(state) | |
} | |
} | |
func startObserving() { | |
StateStore.sharedInstance.add(observer: self) | |
isObserving = true | |
} | |
func stopObserving() { | |
StateStore.sharedInstance.remove(observer: self) | |
isObserving = false | |
} | |
} | |
protocol Action { | |
func execute(with state: AppState) -> AppState | |
} | |
protocol Dispatcher { | |
func dispatch(_ action: Action) | |
} | |
protocol Store { | |
var state: State {get} | |
func add(observer: StateObserver) | |
func remove(observer: StateObserver) | |
} | |
class StateStore: Store { | |
static let sharedInstance = StateStore() | |
let queue = DispatchQueue(label: "state-store-dispatch-queue") | |
let source: DispatchSourceUserDataAdd | |
private var observers: [StateObserver] = [] | |
private var _state = AppState() | |
var state: State { | |
return queue.sync { _state } | |
} | |
private init() { | |
source = DispatchSource.userDataAdd(queue: queue) | |
source.setEventHandler { [unowned self] in | |
self.informObserversOfStateChange(with: self._state) | |
} | |
source.activate() | |
} | |
func add(observer: StateObserver) { | |
queue.async { | |
self.observers.append(observer) | |
} | |
} | |
func remove(observer: StateObserver) { | |
queue.async { | |
if let index = self.observers.index(where: { $0 === observer }) { | |
self.observers.remove(at: index) | |
} | |
} | |
} | |
} | |
extension StateStore: Dispatcher { | |
func dispatch(_ action: Action) { | |
queue.async { | |
self._state = action.execute(with: self._state) | |
self.source.mergeData(value: 1) | |
} | |
} | |
private func informObserversOfStateChange(with state: State) { | |
dispatchPrecondition(condition: .onQueue(queue)) | |
for observer in self.observers { | |
observer.queue.async { | |
observer.stateDidChange(state) | |
} | |
} | |
} | |
} | |
enum CounterAction: Action { | |
case increment(by: Int) | |
case decrement(by: Int) | |
func execute(with state: AppState) -> AppState { | |
var mutableState = state | |
switch self { | |
case .increment(let amount): | |
mutableState.counter += amount | |
case .decrement(let amount): | |
mutableState.counter -= amount | |
} | |
return mutableState | |
} | |
} | |
class Observer: StateObserver { | |
var isObserving = false | |
init() { | |
startObserving() | |
} | |
func stateDidChange(_ state: State) { | |
print(state) | |
} | |
func incrementCounter() { | |
let action = CounterAction.increment(by: 1) | |
StateStore.sharedInstance.dispatch(action) | |
} | |
func decrementCounter() { | |
let action = CounterAction.decrement(by: 1) | |
StateStore.sharedInstance.dispatch(action) | |
} | |
deinit { | |
// the observer is resposnsible ensuring sure it has stopped observing state before it is dealloc'd | |
// if this was a UIViewController, you would normally call stopObserving in viewWillDisappear | |
precondition(isObserving == false) | |
} | |
} | |
// MARK:- In use | |
let anObserver = Observer() | |
anObserver.incrementCounter() | |
// here we put an artificial delay between counter state changes to simulate a user interacting with the UI | |
// when there's around 100ms or more delay between the actions, the DispatchSource will dispatch a separate event | |
// for each change, if there's less, the DispatchSource will sometimes choose to coalesce the changes into a single event | |
DispatchQueue.main.after(when: DispatchTime.now() + 0.1) { | |
anObserver.incrementCounter() | |
anObserver.decrementCounter() | |
anObserver.incrementCounter() | |
} | |
DispatchQueue.main.after(when: DispatchTime.now() + 0.2) { | |
anObserver.incrementCounter() | |
} | |
DispatchQueue.main.after(when: DispatchTime.now() + 0.3) { | |
anObserver.incrementCounter() | |
} | |
DispatchQueue.main.after(when: DispatchTime.now() + 0.5) { | |
anObserver.incrementCounter() | |
} | |
DispatchQueue.main.after(when: DispatchTime.now() + 0.6) { | |
anObserver.incrementCounter() | |
anObserver.decrementCounter() | |
anObserver.decrementCounter() | |
} | |
anObserver.finishObserving() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment