☕ Dripper

Dripper 是一个用于 SwiftUI 项目的架构框架。
它是一个轻量级框架,专注于 Point-Free 的 Swift-Composable-Architecture 的核心概念。

以下是我们需要的核心概念

  1. 在其父状态/动作中使用微状态/动作的逆变。 ❌
  2. 用于简洁状态处理的单向突变流。 ✅
  3. 易于划分责任,易于单元测试。 🏗️

与 TCA 有什么不同?

我们希望尽可能多地使用原生 Swift 功能,因此我们决定使用 @Observable 而不是像 TCA 中 @ObservableState 那样的自定义观察机制。
遗憾的是,由于 @Observable 的限制,这意味着我们无法使用可靠的基于结构体的状态管理。
@Observable 目前仅支持基于类的属性,因此我们不得不为我们的 State 使用类。
一旦 Swift 支持基于类的属性,我们将考虑迁移到基于结构体(或基于 Actor)的状态管理。

如何使用?

它基本上类似于原始的 TCA,但进行了一些简化。
这是一个简单的例子

Dripper

首先,我们必须创建一个符合 Dripper 协议的 Dripper 结构体。
它的作用等同于 TCA 中的 Reducer

import Dripper

struct Counter: Dripper {
    @Observable
    final class State: @unchecked Sendable {
        var count = 0
        @ObservationIgnored private let id: UUID

        init(count: Int = .zero) {
            self.count = count
            self.id = UUID()
        }
    }

    enum Action {
        case increase
        case decrease
    }

    var body: some Dripper<State, Action> {
        Drip { state, action in
            switch action {
            case .increase:
                state.count += 1
                return .none

            case .decrease:
                state.count -= 1
                return .none
            }
        }
    }
}

注意

您需要将 @unchecked Sendable 添加到 State 类以抑制编译器错误。虽然 State 本身实际上不是线程安全的,但在 Station 中使用时,由于它由 StateHandler actor 管理,因此保证是线程安全的。

我们将在未来的更新中为此实现更好的解决方案。此外,欢迎随时就此问题提出任何改进建议! 😊

Station

要在您的 SwiftUI 视图中使用 Dripper,请创建一个以 Dripper 作为其泛型类型参数的 Station 实例。

import SwiftUI
import Dripper

struct ContentView: View {
    let station: StationOf<Counter>
}

#Preview {
    CounterView(
        station: Station(initialState: Counter.State()) {
            Counter()
        }

        Button("\(station.count)") {
            station.pour(.increase)
        }
    )
}

您可以使用 pour 方法触发 Action,并通过 Station 属性直接访问状态。

Effects

Effect 帮助您处理副作用,例如异步操作。
还记得我们在 Dripper 示例中看到的 .none 吗?
实际上,那是 Effect 之一,表示不会发生副作用。

这是一个关于如何使用 Effect 的示例

import Dripper

var body: some Dripper<State, Action> {
    Drip { state, action in
        switch action {
        case .increase:
            state.count += 1
            return .none // means no side-effect

        case .decrease:
            state.count -= 1
            return .run { pour in // means there's a side-effect
                let score = try await fetchScore(for: .now)

                let action = score.isPositive ? Action.increase : Action.decrease
                pour(action) // you can trigger another action
            }
        }
    }
}

要处理副作用,请将 .run 与接收 pour 函数的闭包一起使用。
在此闭包内,您可以通过使用所需的 Action 作为参数调用 pour 来触发其他操作。


感谢您查看 Dripper! 欢迎提出问题和贡献 😊

MIT 许可证 - LICENSE