Skip to content

Commit 5be6b0d

Browse files
authored
Add changes modifier (#62)
* Add changes modifier * Update docc * Use changes modifier in the example app * Add tests * Update README
1 parent 357f284 commit 5be6b0d

File tree

7 files changed

+165
-6
lines changed

7 files changed

+165
-6
lines changed

Examples/Packages/iOS/Sources/ExampleVoiceMemo/Atoms/VoiceMemoListAtoms.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ struct RecordingDataAtom: StateAtom, Hashable {
151151

152152
struct RecordingElapsedTimeAtom: PublisherAtom, Hashable {
153153
func publisher(context: Context) -> AnyPublisher<TimeInterval, Never> {
154-
let isRecording = context.watch(IsRecordingAtom())
154+
let isRecording = context.watch(IsRecordingAtom().changes)
155155

156156
guard isRecording else {
157157
return Just(.zero).eraseToAnyPublisher()

Examples/Packages/iOS/Sources/ExampleVoiceMemo/Atoms/VoiceMemoRowAtoms.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct PlayingElapsedTimeAtom: PublisherAtom {
4141
}
4242

4343
func publisher(context: Context) -> AnyPublisher<TimeInterval, Never> {
44-
let isPlaying = context.watch(IsPlayingAtom(voiceMemo: voiceMemo))
44+
let isPlaying = context.watch(IsPlayingAtom(voiceMemo: voiceMemo).changes)
4545

4646
guard isPlaying else {
4747
return Just(.zero).eraseToAnyPublisher()

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ struct ContactView: View {
665665

666666
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.
667667

668-
#### [select(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/select(_:))
668+
#### [select](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/select(_:))
669669

670670
<details><summary><code>📖 Click to expand example code</code></summary>
671671

@@ -695,6 +695,36 @@ struct CountDisplayView: View {
695695
|Compatible |All atoms types. The selected property must be `Equatable` compliant.|
696696
|Use Case |Performance optimization, Property scope restriction|
697697

698+
#### [changes](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/changes)
699+
700+
<details><summary><code>📖 Click to expand example code</code></summary>
701+
702+
```swift
703+
struct CountAtom: StateAtom, Hashable {
704+
func defaultValue(context: Context) -> Int {
705+
12345
706+
}
707+
}
708+
709+
struct CountDisplayView: View {
710+
@Watch(CountAtom().changes)
711+
var count // : Int
712+
713+
var body: some View {
714+
Text(count.description)
715+
}
716+
}
717+
```
718+
719+
</details>
720+
721+
| |Description|
722+
|:--------------|:----------|
723+
|Summary |Prevents the atom from updating its child views or atoms when its new value is the same as its old value.|
724+
|Output |`T: Equatable`|
725+
|Compatible |All atom types that produce `Equatable` compliant value.|
726+
|Use Case |Performance optimization|
727+
698728
#### [phase](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/phase)
699729

700730
<details><summary><code>📖 Click to expand example code</code></summary>

Sources/Atoms/Atoms.docc/Atoms.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ SwiftUI Atom Properties offers practical capabilities to manage the complexity o
2626
### Modifiers
2727

2828
- ``Atom/select(_:)``
29+
- ``Atom/changes``
2930
- ``Atom/phase``
3031

3132
### Attributes
@@ -66,6 +67,7 @@ SwiftUI Atom Properties offers practical capabilities to manage the complexity o
6667
- ``AtomStore``
6768
- ``AtomModifier``
6869
- ``SelectModifier``
70+
- ``ChangesModifier``
6971
- ``TaskPhaseModifier``
7072
- ``AtomLoader``
7173
- ``RefreshableAtomLoader``
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
public extension Atom where Loader.Value: Equatable {
2+
/// Prevents the atom from updating its child views or atoms when its new value is the
3+
/// same as its old value.
4+
///
5+
/// ```swift
6+
/// struct FlagAtom: StateAtom, Hashable {
7+
/// func defaultValue(context: Context) -> Bool {
8+
/// true
9+
/// }
10+
/// }
11+
///
12+
/// struct ExampleView: View {
13+
/// @Watch(FlagAtom().changes)
14+
/// var flag
15+
///
16+
/// var body: some View {
17+
/// if flag {
18+
/// Text("true")
19+
/// }
20+
/// else {
21+
/// Text("false")
22+
/// }
23+
/// }
24+
/// }
25+
/// ```
26+
///
27+
var changes: ModifiedAtom<Self, ChangesModifier<Loader.Value>> {
28+
modifier(ChangesModifier())
29+
}
30+
}
31+
32+
/// A modifier that prevents the atom from updating its child views or atoms when
33+
/// its new value is the same as its old value.
34+
///
35+
/// Use ``Atom/changes`` instead of using this modifier directly.
36+
public struct ChangesModifier<T: Equatable>: AtomModifier {
37+
/// A type of base value to be modified.
38+
public typealias BaseValue = T
39+
40+
/// A type of modified value to provide.
41+
public typealias Value = T
42+
43+
/// A type representing the stable identity of this atom associated with an instance.
44+
public struct Key: Hashable {}
45+
46+
/// A unique value used to identify the modifier internally.
47+
public var key: Key {
48+
Key()
49+
}
50+
51+
/// Returns a new value for the corresponding atom.
52+
public func modify(value: BaseValue, context: Context) -> Value {
53+
value
54+
}
55+
56+
/// Associates given value and handle updates and cancellations.
57+
public func associateOverridden(value: Value, context: Context) -> Value {
58+
value
59+
}
60+
61+
/// Returns a boolean value that determines whether it should notify the value update to
62+
/// watchers with comparing the given old value and the new value.
63+
public func shouldUpdate(newValue: Value, oldValue: Value) -> Bool {
64+
newValue != oldValue
65+
}
66+
}

Sources/Atoms/Modifier/TaskPhaseModifier.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ public extension Atom where Loader: AsyncAtomLoader {
2626
/// }
2727
/// ```
2828
///
29-
/// This modifier converts the `Task` that the original atom provides into ``AsyncPhase``
30-
/// and notifies its changes to downstream atoms and views.
3129
var phase: ModifiedAtom<Self, TaskPhaseModifier<Loader.Success, Loader.Failure>> {
3230
modifier(TaskPhaseModifier())
3331
}
@@ -38,7 +36,7 @@ public extension Atom where Loader: AsyncAtomLoader {
3836
///
3937
/// Use ``Atom/phase`` instead of using this modifier directly.
4038
public struct TaskPhaseModifier<Success, Failure: Error>: AtomModifier {
41-
/// A type of original value to be modified.
39+
/// A type of base value to be modified.
4240
public typealias BaseValue = Task<Success, Failure>
4341

4442
/// A type of modified value to provide.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import XCTest
2+
3+
@testable import Atoms
4+
5+
@MainActor
6+
final class ChangesModifierTests: XCTestCase {
7+
func testChanges() {
8+
let atom = TestStateAtom(defaultValue: "")
9+
let context = AtomTestContext()
10+
var updatedCount = 0
11+
12+
context.onUpdate = {
13+
updatedCount += 1
14+
}
15+
16+
XCTAssertEqual(updatedCount, 0)
17+
XCTAssertEqual(context.watch(atom.changes), "")
18+
19+
context[atom] = "modified"
20+
21+
XCTAssertEqual(updatedCount, 1)
22+
XCTAssertEqual(context.watch(atom.changes), "modified")
23+
24+
context[atom] = "modified"
25+
26+
// Should not be updated with an equivalent value.
27+
XCTAssertEqual(updatedCount, 1)
28+
}
29+
30+
func testKey() {
31+
let modifier = ChangesModifier<Int>()
32+
33+
XCTAssertEqual(modifier.key, modifier.key)
34+
XCTAssertEqual(modifier.key.hashValue, modifier.key.hashValue)
35+
}
36+
37+
func testShouldUpdate() {
38+
let modifier = ChangesModifier<Int>()
39+
40+
XCTAssertFalse(modifier.shouldUpdate(newValue: 100, oldValue: 100))
41+
XCTAssertTrue(modifier.shouldUpdate(newValue: 100, oldValue: 200))
42+
}
43+
44+
func testModify() {
45+
let atom = TestValueAtom(value: 0)
46+
let modifier = ChangesModifier<Int>()
47+
let transaction = Transaction(key: AtomKey(atom)) {}
48+
let context = AtomModifierContext<Int>(transaction: transaction) { _ in }
49+
let value = modifier.modify(value: 100, context: context)
50+
51+
XCTAssertEqual(value, 100)
52+
}
53+
54+
func testAssociateOverridden() {
55+
let atom = TestValueAtom(value: 0)
56+
let modifier = ChangesModifier<Int>()
57+
let transaction = Transaction(key: AtomKey(atom)) {}
58+
let context = AtomModifierContext<Int>(transaction: transaction) { _ in }
59+
let value = modifier.associateOverridden(value: 100, context: context)
60+
61+
XCTAssertEqual(value, 100)
62+
}
63+
}

0 commit comments

Comments
 (0)