Skip to content

Commit d6b4e72

Browse files
committed
📝 Update README.md
1 parent 99c3a2f commit d6b4e72

2 files changed

Lines changed: 120 additions & 2 deletions

File tree

Examples/Counter/Counter/Counter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ struct CounterView: View {
9898

9999
// MARK: Properties
100100

101-
var station: StationOf<Counter>
101+
let station: StationOf<Counter>
102102

103103
// MARK: Content
104104

README.md

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,124 @@
77
It's a lightweight framework focusing on a very core concepts of [Swift-Composable-Architecture](https://github.com/pointfreeco/swift-composable-architecture) from Point-Free.
88

99
These are the core concepts we needed:
10-
1. Contravariance usage of micro-states/actions within its parent state/action. ❌
10+
1. Contravariance usage of micro-states/actions within its parent state/action. ❌
1111
2. Unidirectional mutation flow for concise state handling. ✅
1212
3. Simple to divide responsibility, simple to unit-test. 🏗️
13+
14+
## What's the difference from TCA?
15+
16+
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.\
17+
Sadly, this means that we can't use solid struct-based state management because of the limitation of `@Observable`.\
18+
`@Observable` currently only supports class-based properties, so we had to use class for our `State`.\
19+
Once Swift supports class-based properties, we will consider migrating to struct-based (or actor-based) state management.
20+
21+
22+
## How to use?
23+
24+
It's basically similar to the original TCA, but with a little bit of simplification.\
25+
Here's a simple example:
26+
27+
### Dripper
28+
First, we have to create a `Dripper` struct that conforms to `Dripper` protocol.\
29+
It has a role equivalent to `Reducer` in TCA.
30+
```swift
31+
import Dripper
32+
33+
struct Counter: Dripper {
34+
@Observable
35+
final class State: @unchecked Sendable {
36+
var count = 0
37+
@ObservationIgnored private let id: UUID
38+
39+
init(count: Int = .zero) {
40+
self.count = count
41+
self.id = UUID()
42+
}
43+
}
44+
45+
enum Action {
46+
case increase
47+
case decrease
48+
}
49+
50+
var body: some Dripper<State, Action> {
51+
Drip { state, action in
52+
switch action {
53+
case .increase:
54+
state.count += 1
55+
return .none
56+
57+
case .decrease:
58+
state.count -= 1
59+
return .none
60+
}
61+
}
62+
}
63+
}
64+
```
65+
66+
> [!NOTE]
67+
> `State` class should be annotated with `@unchecked Sendable` to suppress the compiler error.\
68+
> This is because `State` actually cannot be `Sendable` standalone.\
69+
> However, while using `State` within `Station`, it's guaranteed to be thread-safe because it is managed by actor called `StateHandler`.
70+
>
71+
> We'll find some workaround for this in the future.
72+
73+
### Station
74+
In SwiftUI view, you can use `Dripper` by using `Station` that uses `Dripper` as its Generic type.
75+
76+
```swift
77+
import SwiftUI
78+
import Dripper
79+
80+
struct ContentView: View {
81+
let station: StationOf<Counter>
82+
}
83+
84+
#Preview {
85+
CounterView(
86+
station: Station(initialState: Counter.State()) {
87+
Counter()
88+
}
89+
90+
Button("\(station.count)") {
91+
station.pour(.increase)
92+
}
93+
)
94+
}
95+
96+
You can trigger `Action` with `pour` method, and you can observe the state with just accessing the property of `station`.
97+
98+
### Effects
99+
100+
You can use `Effect` to handle side-effects like async operation.\
101+
Actually, `.none` you saw in the `Dripper` section is one of `Effect` which is meaning there's no side-effect.
102+
103+
Here's an example of how to use `Effect`:
104+
105+
```swift
106+
import Dripper
107+
108+
var body: some Dripper<State, Action> {
109+
Drip { state, action in
110+
switch action {
111+
case .increase:
112+
state.count += 1
113+
return .none // means no side-effect
114+
115+
case .decrease:
116+
state.count -= 1
117+
return .run { pour in // means there's a side-effect
118+
let score = try await fetchScore(for: .now)
119+
120+
let action = score.isPositive ? Action.increase : Action.decrease
121+
pour(action) // you can trigger another action
122+
}
123+
}
124+
}
125+
}
126+
```
127+
128+
You can use `.run` to handle side-effect.\
129+
It takes a closure that takes `pour` as an argument.\
130+
You can trigger another action by calling `pour` inside the closure.

0 commit comments

Comments
 (0)