Skip to content

Instantly share code, notes, and snippets.

@jon-cotton
Last active June 23, 2016 16:46
Show Gist options
  • Save jon-cotton/6c2d8d8a6cab4dbefae764aaa22381b2 to your computer and use it in GitHub Desktop.
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/
//: 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