Skip to content

Commit b85fd84

Browse files
authored
Previous modifier (#198)
* Implement previous modifier * Update Atoms.md * Update README
1 parent f88ecba commit b85fd84

File tree

4 files changed

+203
-0
lines changed

4 files changed

+203
-0
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,45 @@ struct ContactView: View {
683683

684684
Modifiers can be applied to an atom to produce a different versions of the original atom to make it more coding friendly or to reduce view re-computation for performance optimization.
685685

686+
#### [previous](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/previous)
687+
688+
| |Description|
689+
|:--------------|:----------|
690+
|Summary |Provides the previous value of the atom instead of the current value.|
691+
|Output |`T?`|
692+
|Compatible |All atom types.|
693+
|Use Case |Track value changes, Compare previous and current values|
694+
695+
<details><summary><code>📖 Example</code></summary>
696+
697+
```swift
698+
struct CounterAtom: StateAtom, Hashable {
699+
func defaultValue(context: Context) -> Int {
700+
0
701+
}
702+
}
703+
704+
struct CounterView: View {
705+
@WatchState(CounterAtom())
706+
var currentValue
707+
708+
@Watch(CounterAtom().previous)
709+
var previousValue
710+
711+
var body: some View {
712+
VStack {
713+
Text("Current: \(currentValue)")
714+
Text("Previous: \(previousValue ?? 0)")
715+
Button("Increment") {
716+
currentValue += 1
717+
}
718+
}
719+
}
720+
}
721+
```
722+
723+
</details>
724+
686725
#### [changes(of:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/changes(of:))
687726

688727
| |Description|

Sources/Atoms/Atoms.docc/Atoms.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Building state by compositing atoms automatically optimizes rendering based on i
2828

2929
### Modifiers
3030

31+
- ``Atom/previous``
3132
- ``Atom/changes``
3233
- ``Atom/changes(of:)``
3334
- ``Atom/animation(_:)``
@@ -84,6 +85,7 @@ Building state by compositing atoms automatically optimizes rendering based on i
8485
- ``AtomStore``
8586
- ``AtomModifier``
8687
- ``AsyncAtomModifier``
88+
- ``PreviousModifier``
8789
- ``ChangesModifier``
8890
- ``ChangesOfModifier``
8991
- ``TaskPhaseModifier``
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
public extension Atom {
2+
/// Provides the previous value of the atom instead of the current value.
3+
///
4+
/// ```swift
5+
/// struct CounterAtom: StateAtom, Hashable {
6+
/// func defaultValue(context: Context) -> Int {
7+
/// 0
8+
/// }
9+
/// }
10+
///
11+
/// struct ExampleView: View {
12+
/// @Watch(CounterAtom())
13+
/// var currentValue
14+
///
15+
/// @Watch(CounterAtom().previous)
16+
/// var previousValue
17+
///
18+
/// var body: some View {
19+
/// VStack {
20+
/// Text("Current: \(currentValue)")
21+
/// Text("Previous: \(previousValue ?? 0)")
22+
/// }
23+
/// }
24+
/// }
25+
/// ```
26+
///
27+
var previous: ModifiedAtom<Self, PreviousModifier<Produced>> {
28+
modifier(PreviousModifier())
29+
}
30+
}
31+
32+
/// A modifier that provides the previous value of the atom instead of the current value.
33+
///
34+
/// Use ``Atom/previous`` instead of using this modifier directly.
35+
public struct PreviousModifier<Base>: AtomModifier {
36+
/// A type of base value to be modified.
37+
public typealias Base = Base
38+
39+
/// A type of value the modified atom produces.
40+
public typealias Produced = Base?
41+
42+
/// A type representing the stable identity of this atom associated with an instance.
43+
public struct Key: Hashable, Sendable {}
44+
45+
/// A unique value used to identify the modifier internally.
46+
public var key: Key {
47+
Key()
48+
}
49+
50+
/// A producer that produces the value of this atom.
51+
public func producer(atom: some Atom<Base>) -> AtomProducer<Produced> {
52+
AtomProducer { context in
53+
context.transaction { context in
54+
let value = context.watch(atom)
55+
let storage = context.watch(StorageAtom())
56+
let previous = storage.previous
57+
storage.previous = value
58+
return previous
59+
}
60+
}
61+
}
62+
}
63+
64+
private extension PreviousModifier {
65+
@MainActor
66+
final class Storage {
67+
var previous: Base?
68+
}
69+
70+
struct StorageAtom: ValueAtom, Hashable {
71+
func value(context: Context) -> Storage {
72+
Storage()
73+
}
74+
}
75+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import XCTest
2+
3+
@testable import Atoms
4+
5+
final class PreviousModifierTests: XCTestCase {
6+
@MainActor
7+
func testPrevious() {
8+
let atom = TestStateAtom(defaultValue: "initial")
9+
let context = AtomTestContext()
10+
11+
XCTAssertNil(context.watch(atom.previous))
12+
13+
// Update the atom value
14+
context[atom] = "second"
15+
16+
// Now previous should return the initial value
17+
XCTAssertEqual(context.watch(atom.previous), "initial")
18+
19+
// Update again
20+
context[atom] = "third"
21+
22+
// Previous should now return "second"
23+
XCTAssertEqual(context.watch(atom.previous), "second")
24+
25+
// Another update
26+
context[atom] = "fourth"
27+
28+
// Previous should now return "third"
29+
XCTAssertEqual(context.watch(atom.previous), "third")
30+
}
31+
32+
@MainActor
33+
func testPreviousWithMultipleWatchers() {
34+
let atom = TestStateAtom(defaultValue: 100)
35+
let context = AtomTestContext()
36+
37+
// Watch both current and previous
38+
XCTAssertEqual(context.watch(atom), 100)
39+
XCTAssertNil(context.watch(atom.previous))
40+
41+
// Update the value
42+
context[atom] = 200
43+
44+
// Check both watchers
45+
XCTAssertEqual(context.watch(atom), 200)
46+
XCTAssertEqual(context.watch(atom.previous), 100)
47+
48+
// Update again
49+
context[atom] = 300
50+
51+
XCTAssertEqual(context.watch(atom), 300)
52+
XCTAssertEqual(context.watch(atom.previous), 200)
53+
}
54+
55+
@MainActor
56+
func testPreviousUpdatesDownstream() {
57+
let atom = TestStateAtom(defaultValue: 0)
58+
let context = AtomTestContext()
59+
var updatedCount = 0
60+
61+
context.onUpdate = {
62+
updatedCount += 1
63+
}
64+
65+
// Initial watch
66+
XCTAssertEqual(updatedCount, 0)
67+
XCTAssertNil(context.watch(atom.previous))
68+
69+
// First update
70+
context[atom] = 1
71+
XCTAssertEqual(updatedCount, 1)
72+
XCTAssertEqual(context.watch(atom.previous), 0)
73+
74+
// Second update
75+
context[atom] = 2
76+
XCTAssertEqual(updatedCount, 2)
77+
XCTAssertEqual(context.watch(atom.previous), 1)
78+
}
79+
80+
@MainActor
81+
func testKey() {
82+
let modifier = PreviousModifier<Int>()
83+
84+
XCTAssertEqual(modifier.key, modifier.key)
85+
XCTAssertEqual(modifier.key.hashValue, modifier.key.hashValue)
86+
}
87+
}

0 commit comments

Comments
 (0)