Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/gre4ixin/reduxui
- Owner: gre4ixin
- License: mit
- Created: 2021-10-28T21:40:09.000Z (about 3 years ago)
- Default Branch: main
- Last Pushed: 2022-01-08T22:55:01.000Z (almost 3 years ago)
- Last Synced: 2024-09-22T01:04:03.720Z (about 2 months ago)
- Topics: architecture, asyncawait, combine, concurrency, flux, flux-architecture, framework, ios, redux, redux-thunk, state-machine, swift, swiftpackagemanager, swiftui
- Language: Swift
- Homepage:
- Size: 5.85 MB
- Stars: 43
- Watchers: 2
- Forks: 5
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
![logo](./logo.png)
# Simple Architecture like Redux
## Installation
### SPM
```swift
dependencies: [
.package(url: "https://github.com/gre4ixin/ReduxUI.git", .upToNextMinor(from: "1.0.0"))
]
```## Usage
```swift
import ReduxUIclass 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 = AppActionfunc 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: Storevar 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 Combineprotocol 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`.