Skip to content

Instantly share code, notes, and snippets.

@jon-cotton
Created April 20, 2016 10:28
Show Gist options
  • Save jon-cotton/7ba8b323815f3d6a01583a0b6829169b to your computer and use it in GitHub Desktop.
Save jon-cotton/7ba8b323815f3d6a01583a0b6829169b to your computer and use it in GitHub Desktop.
import Foundation
import XCPlayground
/*:
# Reflux
*/
//: Interfaces
protocol State {
init()
var userState: UserState {get}
}
protocol MutableState: State {
var userState: UserState {get set}
}
protocol StateSubscriber: class {
func applyState(state: State)
}
protocol Action {
func execute(state: MutableState) -> MutableState
}
protocol ActionCreating {
func createAction(state: State, completion: (Action?) -> ())
}
protocol ActionCreatingAction: ActionCreating, Action {}
protocol Store {
var state: State {get}
func addStateSubscriber(subscriber: StateSubscriber)
func removeStateSubscriber(subscriber: StateSubscriber)
}
protocol Dispatcher {
func dispatch(action: Action)
func dispatch(actionCreating: ActionCreating)
func dispatch(actionCreatingAction: ActionCreatingAction)
}
//: Store
final class AppStore: Store {
static let sharedInstance = AppStore(initialState: AppState())
private let dispatchQueue = dispatch_queue_create("redux-store-dispatch-q", DISPATCH_QUEUE_SERIAL)
private var _state: MutableState
var state: State {
var state: State!
dispatch_sync(dispatchQueue) {
state = self._state
}
return state
}
private var subscribers: [StateSubscriber] = []
init(initialState: MutableState) {
_state = initialState
}
func addStateSubscriber(subscriber: StateSubscriber) {
dispatch_async(dispatchQueue) {
self.subscribers.append(subscriber)
}
}
func removeStateSubscriber(subscriber: StateSubscriber) {
if let index = subscribers.indexOf({ return $0 === subscriber }) {
dispatch_async(dispatchQueue) {
self.subscribers.removeAtIndex(index)
}
}
}
}
//: Dispatcher
extension AppStore: Dispatcher {
func dispatch(action: Action) {
dispatch_async(dispatchQueue) {
let newState = action.execute(self._state)
self._state = newState
for subsciber in self.subscribers {
subsciber.applyState(self._state)
}
}
}
func dispatch(actionCreating: ActionCreating) {
actionCreating.createAction(state) { [weak self] action in
guard let action = action
else { return }
self?.dispatch(action)
}
}
func dispatch(actionCreatingAction: ActionCreatingAction) {
dispatch(actionCreatingAction as ActionCreating)
dispatch(actionCreatingAction as Action)
}
}
//: State
struct AppState: MutableState {
var userState = UserState()
}
//: Helpers
protocol StoreConsumer {
var store: AppStore {get}
}
extension StoreConsumer {
var store: AppStore {
return AppStore.sharedInstance
}
}
//: Store Tests
func test_store_dispatch_queue() {
struct BlockingAction: Action {
let semaphore = dispatch_semaphore_create(0)
func execute(state: MutableState) -> MutableState {
var mutableState = state
mutableState.userState.user?.firstName = "Bobby"
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(5 * Double(NSEC_PER_SEC)))
dispatch_semaphore_wait(semaphore, delayTime)
return mutableState
}
}
struct NonBlockingAction: Action {
func execute(state: MutableState) -> MutableState {
var mutableState = state
mutableState.userState.user?.firstName = "Barry"
return mutableState
}
}
var state = AppState()
var user = User()
user.firstName = "Bobby"
state.userState.user = user
let store = AppStore(initialState: state)
let firstAction = BlockingAction()
let secondAction = NonBlockingAction()
let concurrentQueue = dispatch_queue_create("testQ", DISPATCH_QUEUE_CONCURRENT)
dispatch_async(concurrentQueue) {
store.dispatch(firstAction)
}
dispatch_async(concurrentQueue) {
store.dispatch(secondAction)
}
// wait 6 seconds to ensure both actions have executed, irl this would be implemented better
let semaphore = dispatch_semaphore_create(0)
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(6 * Double(NSEC_PER_SEC)))
dispatch_semaphore_wait(semaphore, delayTime)
assert(store.state.userState.user?.firstName == "Barry" , "Actual Value: \(store.state.userState.user?.firstName)")
}
//: Login Feature
struct UserState {
var user: User?
var loginRequested = false
var loginError: ErrorType?
}
struct User {
var username = ""
var firstName = ""
var lastName = ""
}
enum UserAction: Action {
case requestLogin
case userSuccesfullyLoggedIn(user: User)
case loginFailed(error: ErrorType)
func execute(state: MutableState) -> MutableState {
var appState = state
var userState = appState.userState
switch self {
case .requestLogin:
userState.loginRequested = true
case .userSuccesfullyLoggedIn(let user):
userState.loginRequested = false
userState.user = user
case .loginFailed(let error):
userState.loginRequested = false
userState.loginError = error
}
appState.userState = userState
return appState
}
}
//: Login Feature Tests
func test_request_login_action() {
let action = UserAction.requestLogin
var state = AppState()
state.userState.loginRequested = false
let newState = action.execute(state)
assert(newState.userState.loginRequested == true)
}
func test_user_succesfully_logged_in_action() {
var testUser = User()
testUser.firstName = "Test"
testUser.lastName = "User"
testUser.username = "testuser"
let action = UserAction.userSuccesfullyLoggedIn(user: testUser)
var state = AppState()
state.userState.user = nil
state.userState.loginRequested = true
let newState = action.execute(state)
assert(newState.userState.user?.firstName == testUser.firstName)
assert(newState.userState.user?.lastName == testUser.lastName)
assert(newState.userState.user?.username == testUser.username)
assert(newState.userState.loginRequested == false)
}
func test_login_failed_action() {
struct TestError: ErrorType {}
let action = UserAction.loginFailed(error: TestError())
var state = AppState()
state.userState.loginRequested = true
let newState = action.execute(state)
assert(newState.userState.loginRequested == false)
}
//: Test Runner
test_request_login_action()
test_user_succesfully_logged_in_action()
test_login_failed_action()
test_store_dispatch_queue()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment