CI

Puredux 是一个适用于 SwiftUI 和 UIKit 的架构框架,旨在通过关注单向数据流和关注点分离来简化状态管理。

为何选择 Puredux?

使用 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    |        |
                +----------------+                   +----+---+
        

Store 定义

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

UI 绑定

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())
            }
    }
}
点击展开 UIViewController 示例

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

分层 Store 树架构

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 确保它们保持不同。

Store 注入

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 中构建功能时遇到的几个常见挑战,并提供了解决这些挑战的解决方案。

Reducers 执行

Puredux 的设计方式允许您在没有任何依赖项的情况下实现状态和 reducers。

这样做是有意为之的,目的是能够将所有 store 的 reducers 工作卸载到后台,而无需过多担心数据竞争、与依赖项的访问同步。

因此,reducers 在后台运行,从而将主线程专门留给 UI。

QoS 调优

创建根 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
                }
            )
    }
}
点击展开 UIViewController 示例

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 更新防抖

在某些情况下,频繁的状态更新是不可避免的,但为每次更新触发 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
                }
            )
    }
}
点击展开 UIViewController 示例

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

带有后台卸载的两步 UI 更新

一个非常常见的用例是将原始模型数据转换为更易于呈现的格式,其中可能包括映射 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
                }
            )
    }
}
点击展开 UIViewController 示例

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

精细的 UI 更新

Puredux 提供了对 UI 更新的完全控制。 任何 SwiftUI ViewUIViewControllerUIView 都可以单独订阅 Store,并具有状态去重、防抖和带有后台卸载的两步 UI 更新。

安装

Swift Package Manager。

Puredux 可通过 Swift Package Manager 获得。

要安装它,请在 Xcode 11.0 或更高版本中选择 File > Swift Packages > Add Package Dependency... 并为所需的模块添加 Puredux 存储库 URL

https://github.com/KazaiMazai/Puredux

文档

许可

Puredux 及其所有模块均已获得 MIT 许可。