Skip to content

Instantly share code, notes, and snippets.

@fxm90
Last active Nov 23, 2020
Embed
What would you like to do?
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 Combine
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(oldState: CounterState, action: CounterAction) -> CounterState {
switch action {
case .increase:
return CounterState(value: oldState.value + 1)
case .decrease:
return CounterState(value: oldState.value - 1)
default:
// Nothing to do here.
return oldState
}
}
// 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.
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(oldState: 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(GradientBackgroundStyle())
Button("Sync. Decrease") {
self.store.dispatch(action: .decrease)
}
.buttonStyle(GradientBackgroundStyle())
Button("Async. Increase (1.0s Delay)") {
self.store.dispatch(action: .asyncIncrease)
}
.buttonStyle(GradientBackgroundStyle())
Button("Async. Decrease (1.5s Delay)") {
self.store.dispatch(action: .asyncDecrease)
}
.buttonStyle(GradientBackgroundStyle())
}
.padding(16)
}
}
// MARK: - Helpers
struct GradientBackgroundStyle: ButtonStyle {
// MARK: - Config
private enum Config {
static let gradientColors = [
Color(red: 0.07, green: 0.60, blue: 0.56),
Color(red: 0.22, green: 0.94, blue: 0.49),
]
}
// MARK: - Public methods
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.padding(.horizontal, 20)
.foregroundColor(.white)
.background(
LinearGradient(gradient: Gradient(colors: Config.gradientColors),
startPoint: .leading,
endPoint: .trailing))
.cornerRadius(40)
}
}
// 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