Simple Architecture like Redux
Installation
SPM
dependencies: [
.package(url: "https://github.com/gre4ixin/ReduxUI.git", .upToNextMinor(from: "1.0.0"))
]
Usage
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
.
Middlewares
calls after reducer function and return
AnyPublisher<MiddlewareAction, Never>
https://jsonplaceholder.typicode.com/users
.
For example create simple project who fetch users from 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.
Simple network request without error checking
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()
}
}
State
, Action
and Reducer
Make 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
}
}
}
users DTO
.
Middleware for make network request and return 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>])
Deferred Action
.
You can return 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
.