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

Reference