Skip to content

Instantly share code, notes, and snippets.

@fxm90
Last active December 12, 2022 10:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fxm90/c3f74f2c695377b17b1f80cf96a31114 to your computer and use it in GitHub Desktop.
Save fxm90/c3f74f2c695377b17b1f80cf96a31114 to your computer and use it in GitHub Desktop.
Playground showing how to use Redux with SwiftUI.
//
// 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