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
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
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
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)
}
}