Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/gre4ixin/reduxui

💎 Redux like architecture for SwiftUI
https://github.com/gre4ixin/reduxui

architecture asyncawait combine concurrency flux flux-architecture framework ios redux redux-thunk state-machine swift swiftpackagemanager swiftui

Last synced: 9 days ago
JSON representation

💎 Redux like architecture for SwiftUI

Awesome Lists containing this project

README

        

![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`.