How to make simple Redux for SwiftUI

Issue #502

Mutation is used to mutate state synchronously. Action is like intent, either from app or from user action. Action maps to Mutation in form of Publisher to work with async action, similar to redux-observable

AnyReducer is a type erasure that takes the reduce function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import Combine
import Foundation

public protocol Reducer {
associatedtype State
associatedtype Mutation
func reduce(state: State, mutation: Mutation) -> State
}

public struct AnyReducer<State, Mutation> {
public let reduce: (State, Mutation) -> State
public init<R: Reducer>(reducer: R) where R.State == State, R.Mutation == Mutation {
self.reduce = reducer.reduce
}
}

public protocol Action {
associatedtype Mutation
func toMutation() -> AnyPublisher<Mutation, Never>
}

public final class Store<State, Mutation>: ObservableObject {
@Published public private(set) var state: State
public let reducer: AnyReducer<State, Mutation>
public private(set) var cancellables = Set<AnyCancellable>()

public init(initialState: State, reducer: AnyReducer<State, Mutation>) {
self.state = initialState
self.reducer = reducer
}

public func send<A: Action>(action: A) where A.Mutation == Mutation {
action
.toMutation()
.receive(on: DispatchQueue.main)
.sink(receiveValue: update(mutation:))
.store(in: &cancellables)
}

public func update(mutation: Mutation) {
self.state = reducer.reduce(state, mutation)
}
}

To use, conform to all the protocols. Also make typelias AppStore in order to easy specify type in SwiftUI View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import SwiftUI
import Combine

typealias AppStore = Store<AppState, AppMutation>

let appStore: AppStore = AppStore(
initialState: AppState(),
reducer: appReducer
)

struct AppState: Codable {
var hasShownOnboaring = false
}

struct AppReducer: Reducer {
func reduce(state: AppState, mutation: AppMutation) -> AppState {
var state = state
switch mutation {
case .finishOnboarding:
state.hasShownOnboaring = true
@unknown default:
break
}

return state
}
}

enum AppMutation {
case finishOnboarding
}

Use in SwiftUI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct RootScreen: View {
@EnvironmentObject var store: AppStore

var body: some View {
if store.state.hasShownOnboaring {
return Text("Welcome")
.eraseToAnyView()
} else {
return OnboardingScreen()
.eraseToAnyView()
}
}
}

struct OnboardingScreen: View {
@EnvironmentObject var store: AppStore

private func done() {
store.send(action: AppAction.finishOnboarding)
}
}

Reference

Comments