![logo](./logo.png)

Platform Swift License Platform Version

# Simple Architecture like Redux ## Installation ### SPM ```swift dependencies: [ .package(url: "https://github.com/gre4ixin/ReduxUI.git", .upToNextMinor(from: "1.0.0")) ] ``` ## Usage ```swift 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 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`. ##### `Middlewares` calls after reducer function and return ```swift AnyPublisher ``` ##### For example create simple project who fetch users from `https://jsonplaceholder.typicode.com/users`. Create DTO (Decode to object) model ```swift 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. ##### Simple network request without error checking ```swift import Foundation import Combine protocol NetworkWrapperInterface { func request(path: URL, decode: D.Type) -> AnyPublisher } struct NetworkError: Error { let response: URLResponse? let error: Error? } class NetworkWrapper: NetworkWrapperInterface { func request(path: URL, decode: D.Type) -> AnyPublisher { return Deferred { Future { 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() } } ``` ##### Make `State`, `Action` and `Reducer` ```swift 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 } } } ``` ##### Middleware for make network request and return `users DTO`. ```swift 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, Never>? { switch action { case .fetch: return Deferred { Future, 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` ```swift @EnvironmentObject var store: Store 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. ```swift case multiple([MiddlewareAction]) ``` #### You can return `Deferred Action`. ```swift public protocol DeferredAction { associatedtype Action: AnyAction func observe() -> AnyPublisher? func eraseToAnyDeferredAction() -> AnyDeferredAction } ``` 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`.