dependencies: [
.package(url: "https://github.com/gre4ixin/ReduxUI.git", .upToNextMinor(from: "1.0.0"))
]import ReduxUI
class SomeCoordinator: Coordinator {
func perform(_ route: SomeRoute) { }
}
enum SomeRoute: RouteType {
}
enum AppAction: AnyAction {
case increase
case decrease
}
struct AppState: AnyState {
var counter: Int = 0
}
class AppReducer: Reducer {
typealias Action = AppAction
func reduce(_ state: inout AppState, action: AppAction, performRoute: @escaping ((_ route: SomeRoute) -> Void)) {
switch action {
case .increase:
state.counter += 1
case .decrease:
state.counter -= 1
}
}
}
class ContentView: View {
@EnvironmentObject var store: Store<AppState, AppAction, SomeRouter>
var body: some View {
VSTack {
Text(store.state.counter)
Button {
store.dispatch(.increase)
} label: {
Text("increment")
}
Button {
store.dispatch(.decrease)
} label: {
Text("decrement")
}
}
}
}
class AppModuleAssembly {
func build() -> some View {
let reducer = AppReducer().eraseToAnyReducer()
let coordinator = SomeCoordinator().eraseToAnyCoordinator()
let store = Store(initialState: AppState(), coordinator: coordinator, reducer: reducer)
let view = ContentView().environmentObject(store)
return view
}
}That was very simple example, in real life you have to use network request, action in app state changes and many other features. In these cases you can use Middleware.
AnyPublisher<MiddlewareAction, Never>Create DTO (Decode to object) model
struct UserDTO: Decodable, Equatable, Identifiable {
let id: Int
let name: String
let username: String
let phone: String
}Equatable protocol for our state, Identifiable for ForEach generate view in SwiftUI View.
import Foundation
import Combine
protocol NetworkWrapperInterface {
func request<D: Decodable>(path: URL, decode: D.Type) -> AnyPublisher<D, NetworkError>
}
struct NetworkError: Error {
let response: URLResponse?
let error: Error?
}
class NetworkWrapper: NetworkWrapperInterface {
func request<D: Decodable>(path: URL, decode: D.Type) -> AnyPublisher<D, NetworkError> {
return Deferred {
Future<D, NetworkError> { promise in
let request = URLRequest(url: path)
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let _ = self else { return }
if let _error = error {
promise(.failure(NetworkError(response: response, error: _error)))
}
guard let unwrapData = data, let json = try? JSONDecoder().decode(decode, from: unwrapData) else {
promise(.failure(NetworkError(response: response, error: error)))
return
}
promise(.success(json))
}.resume()
}
}.eraseToAnyPublisher()
}
}enum AppAction: AnyAction {
case fetch
case isLoading
case loadingEnded
case updateUsers([UserDTO])
case error(message: String)
}
struct AppState: AnyState {
var users: [UserDTO] = []
var isLoading = false
var errorMessage = ""
}
class AppReducer: Reducer {
typealias Action = AppAction
func reduce(_ state: inout AppState, action: AppAction, performRoute: @escaping ((RouteWrapperAction) -> Void)) {
switch action {
case .fetch:
state.isLoading = true
state.errorMessage = ""
case .isLoading:
state.isLoading = true
case .loadingEnded:
state.isLoading = false
case .updateUsers(let users):
state.users = users
state.isLoading = false
state.errorMessage = ""
case .error(let message):
state.errorMessage = message
}
}
}class AppMiddleware: Middleware {
typealias State = AppState
typealias Action = AppAction
typealias Router = RouteWrapperAction
let networkWrapper: NetworkWrapperInterface
var cancelabels = CombineBag()
init(networkWrapper: NetworkWrapperInterface) {
self.networkWrapper = networkWrapper
}
func execute(_ state: AppState, action: AppAction) -> AnyPublisher<MiddlewareAction<AppAction, RouteWrapperAction>, Never>? {
switch action {
case .fetch:
return Deferred {
Future<MiddlewareAction<AppAction, RouteWrapperAction>, Never> { [weak self] promise in
guard let self = self else { return }
self.networkWrapper
.request(path: URL(string: "https://jsonplaceholder.typicode.com/users")!, decode: [UserDTO].self)
.sink { result in
switch result {
case .finished: break
case .failure(let error):
promise(.success(.performAction(.error(message: "Something went wrong!"))))
}
} receiveValue: { dto in
promise(.success(.performAction(.updateUsers(dto))))
}.store(in: &self.cancelabels)
}
}.eraseToAnyPublisher()
default:
return nil
}
}
}Content View
@EnvironmentObject var store: Store<AppState, AppAction, RouteWrapperAction>
var body: some View {
VStack {
ScrollView {
ForEach(store.state.users) { user in
HStack {
VStack {
Text(user.name)
.padding(.leading, 16)
Text(user.phone)
.padding(.leading, 16)
}
Spacer()
}
Divider()
}
}
Spacer()
if store.state.isLoading {
Text("Loading")
}
if !store.state.errorMessage.isEmpty {
Text(LocalizedStringKey(store.state.errorMessage))
}
Button {
store.dispatch(.fetch)
} label: {
Text("fetch users")
}
}
}When reducer ended his job with action, our store check all added middlewares for some Publishers for curent Action, if Publisher not nil, Store runing that Publisher.
You can return action for reducer and change some data, return action for routing, return .multiple actions.
case multiple([MiddlewareAction<A, R>])public protocol DeferredAction {
associatedtype Action: AnyAction
func observe() -> AnyPublisher<Action, Never>?
func eraseToAnyDeferredAction() -> AnyDeferredAction<A>
}If you want route to Authorization, when your Session Provider send event about dead you session, you can use it action. All you need that conform to protocol DeferredAction you class/struct and erase it to AnyDeferredAction with generic Action.
