Puredux 是一个适用于 SwiftUI 和 UIKit 的架构框架,旨在通过关注单向数据流和关注点分离来简化状态管理。
使用 Puredux 开发应用程序,结合纯粹的状态、动作和 reducer,以及干净的无状态 UI,同时单独管理副作用。
Puredux 的核心遵循一种可预测的状态管理模式,该模式由以下关键组件组成:
+-----------------------------------------+
| Store |
| |
| +-----------+ +-------------------+ |
| | Reducer |<--| Current State | |
New State | +-----------+ +-------------------+ | Actions
<-----------+ | A |<----------+
| | | | | A
| | V | | |
| | +-------------------+ | | |
| | | New State |------------+ | |
| | +-------------------+ | |
| | | |
| +-----------------------------------------+ |
| |
| |
| |
| +----------------+ +---+----+ |
V Observer | | Async Work | | |
+---------->| Side Effects |--------------->| Action |--------->|
| | | Result | | |
| +----------------+ +----+---+ |
| |
| +----------------+ +---+----+ |
V Observer | | User | | |
+---------->| UI |------------------>| Action |------>+
| | Interactions | |
+----------------+ +----+---+
// Let's cover actions with a protocol
protocol Action { }
struct IncrementCounter: Action {
// ...
}
// Define root AppState
struct AppState {
// ...
mutating func reduce(_ action: Action) {
switch action {
case let action as IncrementCounter:
// ...
default:
break
}
}
}
// Inject root store
extension SharedStores {
@StoreEntry var root = StateStore<AppState, Action>(AppState()) { state, action in
state.reduce(action)
}
}
Puredux 可以与 SwiftUI 集成,以管理全局应用程序状态和本地状态。
// View might need a local state that we don't need to share with the whole app
struct ViewState {
// ...
mutating func reduce(_ action: Action) {
// ...
}
}
// In your SwiftUI View, you can combine the app's root store with local view-specific states.
// This allows the view to respond dynamically to both app-level and view-level state changes.
struct ContentView: View {
// We can take an injected root store,
// create a local state store and merge them together.
@State var store: StoreOf(\.root).with(ViewState()) { state, action in
state.reduce(action)
}
@State var viewState: (AppState, ViewState)?
var body: some View {
MyView(viewState)
.subscribe(store) { viewState = $0 }
.onAppear {
dispatch(SomeAction())
}
}
}
// We can do the same thing almost with the same API for UIKit view controller:
struct MyScreenState {
// ...
mutating func reduce(_ action: Action) {
// ...
}
}
final class MyViewController: ViewController {
var store: StoreOf(\.root).with(MyScreenState()) { state, action in
state.reduce(action)
}
override func viewDidLoad() {
super.viewDidLoad()
subscribe(store) { [weak self] in
// Handle updates with the derived props
self?.updateUI(with: $0)
}
}
private func updateUI(with state: (AppState, MyScreenState)) {
// Update UI elements with the new view state
}
}
Puredux 允许您构建分层 store 树,从而促进应用程序的开发,其中状态可以共享或隔离,同时保持状态变化的可预测性。
动作从子 store 向上游传播到根 store,而状态更新从根 store 向下游流动到子 store,最终流向 store 观察者。
store 树层次结构确保业务逻辑与 UI 层完全分离。 这允许构建一个深入、隔离的业务逻辑树,同时保持一个浅层的 UI 层,专注于其自身的职责。
让您的应用程序由业务逻辑驱动,而不是由视图层次结构驱动。
Puredux 提供了灵活的应用程序结构,具有以下选项:
多 store 树是一种灵活的设计,通过以分层结构组织 store 来处理复杂的应用程序。 这种方法具有以下几个优点:
// Root Store Config
let root = StateStore<AppState, Action>(AppState()) { state, action in
state.reduce(action)
}
.effect(\.effectState) { state, dispatch in
Effect {
// ...
}
}
// Feature One Config
let featureOne = root.with(FeatureOne()) { appState, action in
state.reduce(action)
}
.effect(\.effectState) { state, dispatch in
Effect {
// ...
}
}
// Feature Two Config with 2 screen stores
let featureTwo = root.with(FeatureTwo()) { state, action in
state.reduce(action)
}
.effect(\.effectState) { state, dispatch in
Effect {
// ...
}
}
let screenOne = featureTwo.with(ScreenOne()) { state, action in
state.reduce(action)
}
.effect(\.effectState) { state, dispatch in
Effect {
// ...
}
}
let screenTwo = featureTwo.with(ScreenTwo()) { state, action in
state.reduce(action)
}
我们可以将 UI 连接到任何 store,最终会得到以下层次结构:
+----------------+ +--------------+
| AppState Store | -- | Side Effects |
+----------------+ +--------------+
|
|
+------------+-------------------------+
| |
| |
| |
| |
+------------------+ +------------------+ +--------------+
| FeatureOne Store | | FeatureTwo Store | -- | Side Effects |
+------------------+ +------------------+ +--------------+
| | |
+----+ +--------------+ |
| UI | | Side Effects | +------------+------------+
+----+ +--------------+ | |
| |
| |
| |
+-----------------+ +-----------------+
| ScreenOne Store | | ScreenTwo Store |
+-----------------+ +-----------------+
| | |
+----+ +--------------+ +----+
| UI | | Side Effects | | UI |
+----+ +--------------+ +----+
在 UDF 架构的上下文中,“副作用”是指与外部系统或服务交互的异步操作。 这些可能包括:
在 Puredux 框架中,这些副作用的管理通过两种主要机制来实现:
异步动作(Async Actions)专为直接、即发即弃的场景而设计。 它们允许您启动异步操作,并将其无缝集成到您的应用程序逻辑中。 这些动作处理诸如网络请求或数据库操作之类的任务,使您的应用程序能够响应这些操作的结果。 定义执行异步工作的动作,然后在您的应用程序中使用它们。
// Define result and error actions:
struct FetchDataResult: Action {
// ...
}
struct FetchDataError: Action {
// ...
}
// Define async action:
struct FetchDataAction: AsyncAction {
func execute(completeHandler: @escaping (Action) -> Void) {
APIClient.shared.fetchData {
switch $0 {
case .success(let result):
completeHandler(FetchDataResult(result))
case .success(let error):
completeHandler(FetchDataError(error))
}
}
}
}
// When async action is dispatched to the store, it will be executed:
store.dispatch(FetchDataAction())
状态驱动的副作用(State-driven Side Effects)为处理异步操作提供了更高级的功能。 当您需要对执行进行精确控制时,包括重试逻辑、取消以及与 UI 或应用程序其他部分的同步,此机制特别有用。 尽管具有高级功能,但由于其最少的样板代码,它也适用于更简单的用例。
// Add effect state to the state
struct AppState {
private(set) var theJob: Effect.State = .idle()
}
// Add related actions**
enum Action {
case jobSuccess(Something)
case startJob
case cancelJob
case jobFailure(Error)
}
// Handle actions in the reducer
extension AppState {
mutating func reduce(_ action: Action) {
switch action {
case .jobSuccess:
theJob.succeed()
case .startJob:
theJob.run()
case .cancelJob:
theJob.cancel()
case .jobFailure(let error):
theJob.retryOrFailWith(error)
}
}
}
// Add SideEffect to the store:
let store = StateStore<AppState, Action>(AppState()) { state, action in
state.reduce(action)
}
.effect(\.theJob, on: .main) { appState, dispatch in
Effect {
do {
let result = try await apiService.fetch()
dispatch(.jobSuccess(result))
} catch {
dispatch(.jobFailure(error))
}
}
}
// Dispatch action to run the job:
store.dispatch(.startJob)
状态驱动的副作用的一个强大功能是,它们的范围和生命周期由它们连接到的 store 定义。 这使得它们在复杂的 store 层次结构中特别有用,例如应用程序级别的 store、功能范围的 store 和每个屏幕的 store,因为它们的副作用会自动与每个 store 的生命周期对齐。
Puredux 将依赖项分为两类:
虽然两者本质上都是依赖项,但由于它们服务于不同的目的,因此会单独处理,并且 Puredux 确保它们保持不同。
在 SharedStores
扩展中使用 @StoreEntry
来注入 store 实例
extension SharedStores {
@StoreEntry var root = StateStore<AppRootState, Action>(....)
}
// The `@StoreOf` property wrapper can be used to obtain the injected store instance:
struct MyView: View {
@State @StoreOf(\.root)
var store: StateStore<AppRootState, Action>
var body: some View {
// ...
}
}
在 Dependencies
扩展中使用 @DependencyEntry
来注入依赖项实例
extension Dependencies {
@DependencyEntry var now = { Date() }
}
// Then it can be used in the app reducer:
struct AppState {
private var currentTime: Date?
mutating func reduce(_ action: Action) {
switch action {
case let action as UpdateTime:
let now = Dependency[\.now]
currentTime = now()
default:
break
}
}
}
Puredux 提供了一个强大的策略来解决 iOS 应用程序中常见的性能挑战。 它提供了几个关键优化来增强应用程序的响应能力和效率,包括:
随着您的应用程序及其功能的扩展,您可能会遇到性能问题,例如 reducers 执行时间更长或 SwiftUI 视图主体刷新频率超出预期。 本文重点介绍了在 Puredux 中构建功能时遇到的几个常见挑战,并提供了解决这些挑战的解决方案。
Puredux 的设计方式允许您在没有任何依赖项的情况下实现状态和 reducers。
这样做是有意为之的,目的是能够将所有 store 的 reducers 工作卸载到后台,而无需过多担心数据竞争、与依赖项的访问同步。
因此,reducers 在后台运行,从而将主线程专门留给 UI。
创建根 store 时,您可以选择它将在其上运行的队列的服务质量。
let store = StateStore(
InitialState(),
qos: .userInitiated,
reducer: myReducer
)
Puredux 支持状态更改去重,以基于状态特定部分的更改启用精细的 UI 更新。
struct MyView: View {
let store: Store<ViewState, Action>
@State var viewState: ViewState?
var body: some View {
ContentView(viewState)
.subscribe(
store: store,
removeStateDuplicates: .keyPath(\.lastModified),
observe: {
viewState = $0
}
)
}
}
final class MyViewController: ViewController {
var store: StoreOf(\.root).with(MyScreenState()) { state, action in
state.reduce(action)
}
override func viewDidLoad() {
super.viewDidLoad()
subscribe(
store,
removeStateDuplicates: .keyPath(\.lastModified)) { [weak self] in
// Handle updates with the derived props
self?.updateUI(with: $0)
}
}
private func updateUI(with state: (AppState, MyScreenState)) {
// Update UI elements with the new view state
}
}
在某些情况下,频繁的状态更新是不可避免的,但为每次更新触发 UI 可能会消耗太多资源。 为了处理这个问题,Puredux 提供了一个防抖选项。 使用防抖,UI 更新仅在状态更改之间经过指定的时间间隔后才会触发。
struct MyView: View {
let store: Store<ViewState, Action>
@State var viewState: ViewState?
var body: some View {
ContentView(viewState)
.subscribe(
store: store,
debounceFor: 0.016,
observe: {
viewState = $0
}
)
}
}
final class MyViewController: ViewController {
var store: StoreOf(\.root).with(MyScreenState()) { state, action in
state.reduce(action)
}
override func viewDidLoad() {
super.viewDidLoad()
subscribe(store, debounceFor: 0.016) { [weak self] in
self?.updateUI(with: $0)
}
}
private func updateUI(with state: (AppState, MyScreenState)) {
// Update UI elements with the new view state
}
}
一个非常常见的用例是将原始模型数据转换为更易于呈现的格式,其中可能包括映射 AttributedStrings
、使用 DateFormatter
等。 这些操作可能会消耗大量资源。
Puredux 允许您在 store 和 UI 之间的状态更改处理管道中添加一个表示层。 这使您可以将这些计算卸载到后台队列,从而保持 UI 的响应速度。
struct MyView: View {
@State var store: StoreOf(\.root).with(ViewState()) { state, action in
state.reduce(action)
}
@State var viewModel: ViewModel?
var body: some View {
ContentView(viewModel)
.subscribe(
store,
props: { state, dispatch in
// ViewModel will be evaluated on the background presentation queue
ViewModel(state, dispatch)
},
presentationQueue: .sharedPresentationQueue,
observe: {
viewModel = $0
}
)
}
}
final class MyViewController: ViewController {
var store: StoreOf(\.root).with(MyScreenState()) { state, action in
state.reduce(action)
}
override func viewDidLoad() {
super.viewDidLoad()
subscribe(
store,
props: { state, dispatch in
// ViewModel will be evaluated on the background presentation queue
ViewModel(state, dispatch)
},
presentationQueue: .sharedPresentationQueue) { [weak self] in
self?.updateUI(with: $0)
}
}
private func updateUI(with viewModel: ViewModel) {
// Update UI elements with the new view state
}
}
Puredux 提供了对 UI 更新的完全控制。 任何 SwiftUI View
、UIViewController
或 UIView
都可以单独订阅 Store
,并具有状态去重、防抖和带有后台卸载的两步 UI 更新。
Puredux 可通过 Swift Package Manager 获得。
要安装它,请在 Xcode 11.0 或更高版本中选择 File > Swift Packages > Add Package Dependency... 并为所需的模块添加 Puredux 存储库 URL
https://github.com/KazaiMazai/Puredux
Puredux 及其所有模块均已获得 MIT 许可。