
本周,我们将讨论如何创建类似于
Redux使用的状态容器。 它是正在开发的应用程序的唯一价值来源。 整个应用程序的单一状态使调试和验证更加容易。 真值的单一来源消除了在应用程序中创建多个状态时发生的数千个错误。
真相值的单一来源
主要思想是通过单个结构或结构组成来描述整个应用程序的状态。 假设我们正在开发一个Github存储库搜索应用程序,其中state是我们使用Github API根据特定请求选择的存储库数组。
struct AppState { var searchResult: [Repo] = [] }
下一步是将状态(只读)传递给应用程序中的每个视图。 最好的方法是使用SwiftUI的环境。 您还可以将包含整个应用程序状态的对象传递给基本视图的环境。 基本视图将与所有子视图共享环境。 要了解有关SwiftUI环境的更多信息,请查看
SwiftUI出版物中的“环境的力量” 。
final class Store: ObservableObject { @Published private(set) var state: AppState }
在上面的示例中,我们创建了一个
存储对象,该对象存储应用程序的状态并提供对其的只读访问。 State属性使用
@Published属性的包装,该包装将任何更改通知SwiftUI。 它使您可以不断更新整个应用程序,从单个真实值源中派生它。 在前面的文章中,我们讨论了存储对象,要了解更多信息,您需要阅读文章“
在SwiftUI中使用存储对象对应用程序状态进行建模 ”。
减速器和动作
现在该讨论导致状态更改的用户操作。 动作是描述状态变化的简单枚举或枚举的集合。 例如,在数据采样期间设置负载值,将结果存储库分配给state属性,等等。 现在考虑对Action进行枚举的示例代码。
enum AppAction { case search(query: String) case setSearchResult(repos: [Repo]) }
Reducer是一种功能,它采用当前状态,对该状态执行操作,然后生成一个新状态。 通常,
减速器或
减速器的 组成是状态更改的应用程序中唯一的位置。 单个功能可以更改应用程序的整个状态的事实使代码非常简单,易于测试和易于调试。 以下是reduce函数的示例。
struct Reducer<State, Action> { let reduce: (inout State, Action) -> Void } let appReducer: Reducer<AppState, AppAction> = Reducer { state, action in switch action { case let .setSearchResults(repos): state.searchResult = repos } }
单向流
现在该讨论数据流了。 每个视图都可以通过存储对象对状态进行只读访问。 视图可以将操作发送到存储库对象。 Reducer更改状态,然后SwiftUI通知所有状态更改视图。 SwiftUI具有超高效的比较算法,因此显示整个应用程序的状态和更新更改的视图非常快。
状态->视图->操作->状态->视图该体系结构仅围绕单向数据流工作。 这意味着应用程序中的所有数据都遵循相同的模式,这使得创建的应用程序的逻辑更可预测且更易于理解。 让我们更改store对象以支持提交操作。
final class Store<State, Action>: ObservableObject { @Published private(set) var state: State private let appReducer: Reducer<State, Action> init(initialState: State, appReducer: @escaping Reducer<State, Action>) { self.state = initialState self.appReducer = appReducer } func send(_ action: Action) { appReducer.reduce(&state, action) } }
副作用
我们已经实现了一个
单向流,该流可以接受用户操作并更改状态,但是异步操作又如何呢?我们通常将其称为
副作用 。 如何为使用的存储类型添加异步任务支持? 我认为是时候介绍使用
Combine Framework了 ,它是处理异步任务的理想选择。
import Foundation import Combine protocol Effect { associatedtype Action func mapToAction() -> AnyPublisher<Action, Never> } enum SideEffect: Effect { case search(query: String) func mapToAction() -> AnyPublisher<Action, Never> { switch self { case let .search(query): return dependencies.githubService .searchPublisher(matching: query) .replaceError(with: []) .map { AppAction.setSearchResults(repos: $0) } .eraseToAnyPublisher() } } }
通过引入效果协议,我们增加了对
异步任务的支持。
效果是一个动作序列,可以使用
Combine框架的Publisher类型
来发布 。 这使您可以使用Combine处理异步作业,然后发布reducer用于将操作应用于当前状态的操作。
final class Store<State, Action>: ObservableObject { @Published private(set) var state: State private let appReducer: Reducer<State, Action> private var cancellables: Set<AnyCancellable> = [] init(initialState: State, appReducer: Reducer<State, Action>) { self.state = initialState self.appReducer = appReducer } func send(_ action: Action) { appReducer.reduce(&state, action) } func send<E: Effect>(_ effect: E) where E.Action == Action { effect .mapToAction() .receive(on: DispatchQueue.main) .sink(receiveValue: send) .store(in: &cancellables) } }
实际例子
最后,我们可以完成存储库搜索应用程序,该应用程序异步调用
Github API并选择与查询匹配的存储库。 该应用程序的完整源代码可在
Github上找到 。
struct SearchContainerView: View { @EnvironmentObject var store: Store<AppState, AppAction> @State private var query: String = "Swift" var body: some View { SearchView( query: $query, repos: store.state.searchResult, onCommit: fetch ).onAppear(perform: fetch) } private func fetch() { store.send(SideEffect.search(query: query)) } } struct SearchView : View { @Binding var query: String let repos: [Repo] let onCommit: () -> Void var body: some View { NavigationView { List { TextField("Type something", text: $query, onCommit: onCommit) if repos.isEmpty { Text("Loading...") } else { ForEach(repos) { repo in RepoRow(repo: repo) } } }.navigationBarTitle(Text("Search")) } } }
将屏幕分为两个视图:
容器视图和
渲染视图 。
容器视图控制动作并从
全局状态中选择必要的部分。
渲染视图接收数据并显示它们。 我们已经在之前的文章中讨论了容器视图,要了解更多,请点击链接“
在SwiftUI中介绍容器视图 ”
结论
今天,我们学习了如何在创建时考虑到
副作用的类Redux状态容器 。 为此,我们使用了SwiftUI环境功能和Combine框架。 希望本文对您有所帮助。
感谢您的阅读,很快再见!