Last active
December 12, 2022 10:52
-
-
Save fxm90/c3f74f2c695377b17b1f80cf96a31114 to your computer and use it in GitHub Desktop.
Playground showing how to use Redux with SwiftUI.
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
// | |
// Redux.playground | |
// | |
// Created by Felix Mau on 25.06.20. | |
// Copyright © 2020 Felix Mau. All rights reserved. | |
// | |
import PlaygroundSupport | |
import SwiftUI | |
/// # Redux components | |
/// | |
/// **The Store** stores your entire app state in the form of a single data structure. This state can only be | |
/// modified by dispatching Actions to the store. Whenever the state in the store changes, the store will | |
/// notify all observers. | |
/// | |
/// **Actions** are a declarative way of describing a state change. Actions don't contain any code, they are | |
/// consumed by the store and forwarded to reducers. Reducers will handle the actions by implementing a | |
/// different state change for each action. | |
/// | |
/// **Reducers** provide pure functions, that based on the current action and the current app state, create a | |
/// new app state. | |
/// | |
/// **A middleware** is any function that performs side effects when a certain action passes through the Store. | |
/// Those side effects can then trigger new actions to the Store. | |
/// | |
/// Sources: | |
/// - <https://github.com/ReSwift/ReSwift> | |
/// - <https://medium.com/swlh/how-to-handle-asynchronous-operations-with-redux-in-swift-495591d9df84> | |
// MARK: - State | |
/// App-State (Our single source of truth that drives the app). | |
struct CounterState { | |
var value: Int | |
} | |
// MARK: - Action | |
/// Actions to mutate our App-State. | |
enum CounterAction { | |
// Synchronous actions that can be run by the reducer. | |
case increase | |
case decrease | |
// Asynchronous actions that can be run by the middleware. | |
case asyncIncrease | |
case asyncDecrease | |
} | |
// MARK: - Reducer | |
/// This is our reducer, a pure function with `(state, action) => state` signature. | |
/// It describes how an action transforms the state into the next state (**without any side effects**). | |
/// | |
/// This makes it easy to test, as you can (synchronously) verify: | |
/// - Given: Old State | |
/// - When: Action | |
/// - Then: Expect new State | |
func counterReducer(state: CounterState, action: CounterAction) -> CounterState { | |
var mutableState = state | |
switch action { | |
case .increase: | |
mutableState.value += 1 | |
case .decrease: | |
mutableState.value -= 1 | |
case .asyncIncrease, .asyncDecrease: | |
break | |
} | |
return mutableState | |
} | |
// MARK: - Middleware | |
/// A dispatcher is any function that takes in an action. | |
/// | |
/// - Note: This has to match the method `dispatch(action: CounterAction)` on our `Store`. | |
typealias Dispatcher = (CounterAction) -> Void | |
/// A middleware is any function that **performs side effects** when a certain action passes through the Store. | |
/// Those side effects can then trigger new actions to the Store. | |
/// | |
/// This makes it easy to test, as you can verify: | |
/// - Given: Old State | |
/// - When: Action | |
/// - Then: Expect dispatched new Action | |
typealias CounterMiddleware = (CounterState, CounterAction, @escaping Dispatcher) -> Void | |
func asyncCounterMiddleware(dispatchQueue: DispatchQueue) -> CounterMiddleware { | |
return { _, action, dispatch in | |
switch action { | |
case .asyncIncrease: | |
// Simulate async. method call by using a timeout here. | |
dispatchQueue.asyncAfter(deadline: .now() + 1) { | |
dispatch(.increase) | |
} | |
case .asyncDecrease: | |
// Simulate async. method call by using a timeout here. | |
dispatchQueue.asyncAfter(deadline: .now() + 1.5) { | |
dispatch(.decrease) | |
} | |
default: | |
break | |
} | |
} | |
} | |
// MARK: - Store | |
/// Redux store holding the state of our app. | |
final class Store: ObservableObject { | |
// MARK: - Public properties | |
@Published | |
private(set) var counterState: CounterState | |
// MARK: - Private properties | |
private let counterMiddlewares: [CounterMiddleware] | |
// MARK: - Initializer | |
init(counterState: CounterState, counterMiddlewares: [CounterMiddleware]) { | |
self.counterState = counterState | |
self.counterMiddlewares = counterMiddlewares | |
} | |
// MARK: - Public methods | |
/// The only way to mutate the internal state is to dispatch an action. | |
func dispatch(action: CounterAction) { | |
print("ℹ️ – Dispatching action \(action)") | |
// Create new state .. | |
counterState = counterReducer(state: counterState, action: action) | |
// .. and afterwards apply our middlewares. | |
counterMiddlewares.forEach { middleware in | |
middleware(counterState, action, dispatch) | |
} | |
} | |
} | |
// MARK: - View | |
struct CounterView: View { | |
// MARK: - Public properties | |
@EnvironmentObject | |
var store: Store | |
// MARK: - Private properties | |
private var headline: some View { | |
HStack(spacing: 4) { | |
Text("Current value:") | |
Text("\(self.store.counterState.value)") | |
.font(.headline) | |
} | |
} | |
// MARK: - Render | |
var body: some View { | |
VStack(spacing: 16) { | |
headline | |
Button("Sync. Increase") { | |
self.store.dispatch(action: .increase) | |
} | |
.buttonStyle(.borderedProminent) | |
Button("Sync. Decrease") { | |
self.store.dispatch(action: .decrease) | |
} | |
.buttonStyle(.borderedProminent) | |
Button("Async. Increase (1.0s Delay)") { | |
self.store.dispatch(action: .asyncIncrease) | |
} | |
.buttonStyle(.borderedProminent) | |
Button("Async. Decrease (1.5s Delay)") { | |
self.store.dispatch(action: .asyncDecrease) | |
} | |
.buttonStyle(.borderedProminent) | |
} | |
.padding(16) | |
} | |
} | |
// MARK: - Playground setup | |
let initialState = CounterState(value: 0) | |
let counterMiddlewares: [CounterMiddleware] = [ | |
asyncCounterMiddleware(dispatchQueue: .main), | |
] | |
let store = Store(counterState: initialState, | |
counterMiddlewares: counterMiddlewares) | |
let counterView = CounterView() | |
.environmentObject(store) | |
PlaygroundPage.current.setLiveView(counterView) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment