logo

Platform Swift License Platform Version

类似 Redux 的简单架构

安装

SPM

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
    }
}

这是一个非常简单的例子,在实际应用中,您需要使用网络请求,应用状态中的操作变化以及许多其他功能。在这种情况下,您可以使用 Middleware(中间件)。

Middlewares(中间件)在 reducer 函数之后调用并返回。
 AnyPublisher<MiddlewareAction, Never>
例如,创建一个简单的项目,从 https://jsonplaceholder.typicode.com/users 获取用户数据。

创建 DTO (解码为对象) 模型。

struct UserDTO: Decodable, Equatable, Identifiable {
    let id: Int
    let name: String
    let username: String
    let phone: String
}

Equatable 协议用于我们的状态,Identifiable 用于在 SwiftUI View 中通过 ForEach 生成视图。

简单的网络请求,没有错误检查。
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(动作)和 Reducer(归约器)。
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(用户数据传输对象)。
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
        }
    }
}

内容视图

@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")
        }
    }
}

当 reducer 完成了动作的处理后,我们的 store(存储)会检查所有已添加的中间件是否存在针对当前 Action(动作)的 Publishers(发布者)。如果 Publisher 不为空,Store(存储)将会运行该 Publisher。

您可以返回 action(动作)给 reducer 并更改一些数据,返回 action(动作)用于路由,返回 .multiple(多个)动作。

case multiple([MiddlewareAction<A, R>])

您可以返回 Deferred Action(延迟动作)。

public protocol DeferredAction {
    associatedtype Action: AnyAction
    func observe() -> AnyPublisher<Action, Never>?
    
    func eraseToAnyDeferredAction() -> AnyDeferredAction<A>
}

如果您希望在您的 Session Provider 发送会话失效的事件时路由到授权页面,您可以使用该 action(动作)。您只需要让您的 class/struct(类/结构体)遵循 DeferredAction 协议,并使用泛型 Action 将其擦除为 AnyDeferredAction