Dripper is an architecture framework for SwiftUI project.
It's a lightweight framework focusing on a very core concepts of Swift-Composable-Architecture from Point-Free.
These are the core concepts we needed:
- Contravariance usage of micro-states/actions within its parent state/action. ❌
- Unidirectional mutation flow for concise state handling. ✅
- Simple to divide responsibility, simple to unit-test. 🏗️
We wanted to use the native Swift feature as much as possible, so we decided to use @Observable instead of using custom observation mechanism like @ObservableState in TCA.
Sadly, this means that we can't use solid struct-based state management because of the limitation of @Observable.
@Observable currently only supports class-based properties, so we had to use class for our State.
Once Swift supports class-based properties, we will consider migrating to struct-based (or actor-based) state management.
It's basically similar to the original TCA, but with a little bit of simplification.
Here's a simple example:
First, we have to create a Dripper struct that conforms to Dripper protocol.
It has a role equivalent to Reducer in TCA.
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
}
}
}
}Note
You need to add @unchecked Sendable to the State class to suppress compiler errors.
While State itself is actually not thread-safe, when used within Station, it is guaranteed to be thread-safe since it's managed by the StateHandler actor.
We'll implement a better solution for this in a future update. Also, feel free to suggest any improvements on this issue! 😊
To use Dripper in your SwiftUI views, create a Station instance with Dripper as its generic type parameter.
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)
}
)
}You can trigger Action using the pour method and directly access state through the Station properties.
Effect helps you handle side-effects such as asynchronous operations.
Remember the .none we saw in the Dripper example?
Actually, that's one of Effect that indicates no side-effects will occur.
Here's an example of how to use 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
}
}
}
}To handle side-effects, use .run with a closure that receives a pour function.
Inside this closure, you can trigger additional actions by calling pour with desired Action as parameter.
Thanks for checking out Dripper! Questions and contributions are always welcome 😊
MIT license - LICENSE