Skip to content

Commit 98181ab

Browse files
authored
Allow more interactions in updated side effect (#44)
* Allow more interactions in Atom.updated * Update documentation
1 parent b8424b6 commit 98181ab

File tree

9 files changed

+172
-47
lines changed

9 files changed

+172
-47
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,7 +1413,7 @@ struct PersistentCounterAtom: StateAtom, Hashable {
14131413
UserDefaults.standard.integer(forKey: "persistence_key")
14141414
}
14151415

1416-
func updated(newValue: Int, oldValue: Int, reader: Reader) {
1416+
func updated(newValue: Int, oldValue: Int, context: UpdatedContext) {
14171417
if newValue != oldValue {
14181418
UserDefaults.standard.set(newValue, forKey: "persistence_key")
14191419
}
@@ -1423,7 +1423,7 @@ struct PersistentCounterAtom: StateAtom, Hashable {
14231423

14241424
</details>
14251425

1426-
All atom types can optionally implement [`updated(newValue:oldValue:reader:`](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/updated(newvalue:oldvalue:reader:)-fgb8) method to manage arbitrary side-effects of value updates, such as state persistence, state synchronization, logging, and etc.
1426+
All atom types can optionally implement [`updated(newValue:oldValue:context:`](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atom/updated(newvalue:oldvalue:context:)-7kvo0) method to manage arbitrary side-effects of value updates, such as state persistence, state synchronization, logging, and etc.
14271427
In the above example, the initial state of the atom is retrieved from UserDefaults, and when the user updates the state, the value is reflected into UserDefaults as a side effect.
14281428

14291429
---

Sources/Atoms/Atom/Atom.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ public protocol Atom {
1919
/// with other atoms.
2020
typealias Context = AtomTransactionContext<Coordinator>
2121

22-
/// A type of the structure that to read value other atoms.
23-
typealias Reader = AtomReader
22+
/// A type of the context structure that to read, set, and otherwise interacting
23+
/// with other atoms.
24+
typealias UpdatedContext = AtomUpdatedContext
2425

2526
/// A boolean value indicating whether the atom value should be preserved even if
2627
/// no longer watched to.
@@ -51,14 +52,15 @@ public protocol Atom {
5152
///
5253
/// Use it to manage arbitrary side-effects of value updates, such as state persistence,
5354
/// state synchronization, logging, and etc.
54-
/// You can also access other atom values via `reader` passed as a parameter.
55+
/// You can also access other atom values via `context` passed as a parameter.
5556
///
5657
/// - Parameters:
5758
/// - newValue: A new value after update.
5859
/// - oldValue: An old value before update.
59-
/// - reader: A structure that to read value other atoms.
60+
/// - context: A context structure that to read, set, and otherwise
61+
/// interacting with other atoms.
6062
@MainActor
61-
func updated(newValue: Loader.Value, oldValue: Loader.Value, reader: Reader)
63+
func updated(newValue: Loader.Value, oldValue: Loader.Value, context: UpdatedContext)
6264

6365
// --- Internal ---
6466

@@ -76,7 +78,7 @@ public extension Atom {
7678
()
7779
}
7880

79-
func updated(newValue: Loader.Value, oldValue: Loader.Value, reader: Reader) {}
81+
func updated(newValue: Loader.Value, oldValue: Loader.Value, context: UpdatedContext) {}
8082
}
8183

8284
public extension Atom where Self == Key {

Sources/Atoms/Atoms.docc/Atoms.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ SwiftUI Atom Properties offers practical capabilities to manage the complexity o
5656
- ``AtomWatchableContext``
5757
- ``AtomTransactionContext``
5858
- ``AtomViewContext``
59+
- ``AtomUpdatedContext``
5960
- ``AtomTestContext``
60-
- ``AtomReader``
6161

6262
### Internal System
6363

Sources/Atoms/Context/AtomReader.swift

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/// A context structure that to read, set, and otherwise interacting with atoms.
2+
@MainActor
3+
public struct AtomUpdatedContext: AtomContext {
4+
@usableFromInline
5+
internal let _store: StoreContext
6+
7+
internal init(store: StoreContext) {
8+
self._store = store
9+
}
10+
11+
/// Accesses the value associated with the given atom without watching to it.
12+
///
13+
/// This method returns a value for the given atom. Even if you access to a value with this method,
14+
/// it doesn't initiating watch the atom, so if none of other atoms or views is watching as well,
15+
/// the value will not be cached.
16+
///
17+
/// ```swift
18+
/// let context = ...
19+
/// print(context.read(TextAtom())) // Prints the current value associated with `TextAtom`.
20+
/// ```
21+
///
22+
/// - Parameter atom: An atom that associates the value.
23+
///
24+
/// - Returns: The value associated with the given atom.
25+
@inlinable
26+
public func read<Node: Atom>(_ atom: Node) -> Node.Loader.Value {
27+
_store.read(atom)
28+
}
29+
30+
/// Sets the new value for the given writable atom.
31+
///
32+
/// This method only accepts writable atoms such as types conforming to ``StateAtom``,
33+
/// and assign a new value for the atom.
34+
/// When you assign a new value, it notifies update immediately to downstream atoms or views.
35+
///
36+
/// - SeeAlso: ``AtomViewContext/subscript``
37+
///
38+
/// ```swift
39+
/// let context = ...
40+
/// context.set("New text", for: TextAtom())
41+
/// print(context.read(TextAtom())) // Prints "New text"
42+
/// ```
43+
///
44+
/// - Parameters
45+
/// - value: A value to be set.
46+
/// - atom: An atom that associates the value.
47+
@inlinable
48+
public func set<Node: StateAtom>(_ value: Node.Loader.Value, for atom: Node) {
49+
_store.set(value, for: atom)
50+
}
51+
52+
/// Refreshes and then return the value associated with the given refreshable atom.
53+
///
54+
/// This method only accepts refreshable atoms such as types conforming to:
55+
/// ``TaskAtom``, ``ThrowingTaskAtom``, ``AsyncSequenceAtom``, ``PublisherAtom``.
56+
/// It refreshes the value for the given atom and then return, so the caller can await until
57+
/// the value completes the update.
58+
/// Note that it can be used only in a context that supports concurrency.
59+
///
60+
/// ```swift
61+
/// let context = ...
62+
/// let image = await context.refresh(AsyncImageDataAtom()).value
63+
/// print(image) // Prints the data obtained through network.
64+
/// ```
65+
///
66+
/// - Parameter atom: An atom that associates the value.
67+
///
68+
/// - Returns: The value which completed refreshing associated with the given atom.
69+
@discardableResult
70+
@inlinable
71+
public func refresh<Node: Atom>(_ atom: Node) async -> Node.Loader.Value where Node.Loader: RefreshableAtomLoader {
72+
await _store.refresh(atom)
73+
}
74+
75+
/// Resets the value associated with the given atom, and then notify.
76+
///
77+
/// This method resets a value for the given atom, and then notify update to the downstream
78+
/// atoms and views. Thereafter, if any of other atoms or views is watching the atom, a newly
79+
/// generated value will be produced.
80+
///
81+
/// ```swift
82+
/// let context = ...
83+
/// context[TextAtom()] = "New text"
84+
/// print(context.read(TextAtom())) // Prints "New text"
85+
/// context.reset(TextAtom())
86+
/// print(context.read(TextAtom())) // Prints "Text"
87+
/// ```
88+
///
89+
/// - Parameter atom: An atom that associates the value.
90+
@inlinable
91+
public func reset(_ atom: some Atom) {
92+
_store.reset(atom)
93+
}
94+
}

Sources/Atoms/Core/StoreContext.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,8 @@ private extension StoreContext {
384384
}
385385

386386
func notifyUpdated() {
387-
let reader = AtomReader(store: self)
388-
atom.updated(newValue: value, oldValue: oldValue, reader: reader)
387+
let context = AtomUpdatedContext(store: self)
388+
atom.updated(newValue: value, oldValue: oldValue, context: context)
389389
}
390390

391391
// Ensures the value is updated.

Tests/AtomsTests/Atom/ObservableObjectAtomTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ final class ObservableObjectAtomTests: XCTestCase {
2525
TestObject()
2626
}
2727

28-
func updated(newValue: TestObject, oldValue: TestObject, reader: Reader) {
28+
func updated(newValue: TestObject, oldValue: TestObject, context: UpdatedContext) {
2929
onUpdated?(newValue, oldValue)
3030
}
3131
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import XCTest
2+
3+
@testable import Atoms
4+
5+
@MainActor
6+
final class AtomUpdatedContextTests: XCTestCase {
7+
func testRead() {
8+
let atom = TestValueAtom(value: 100)
9+
let store = Store()
10+
let context = AtomUpdatedContext(store: StoreContext(store))
11+
12+
XCTAssertEqual(context.read(atom), 100)
13+
}
14+
15+
func testSet() {
16+
let atom = TestValueAtom(value: 0)
17+
let dependency = TestStateAtom(defaultValue: 100)
18+
let store = Store()
19+
let transaction = Transaction(key: AtomKey(atom)) {}
20+
let storeContext = StoreContext(store)
21+
let context = AtomUpdatedContext(store: storeContext)
22+
23+
XCTAssertEqual(storeContext.watch(dependency, in: transaction), 100)
24+
25+
context.set(200, for: dependency)
26+
27+
XCTAssertEqual(storeContext.watch(dependency, in: transaction), 200)
28+
}
29+
30+
func testRefresh() async {
31+
let atom = TestTaskAtom(value: 100)
32+
let store = Store()
33+
let context = AtomUpdatedContext(store: StoreContext(store))
34+
35+
let value = await context.refresh(atom).value
36+
37+
XCTAssertEqual(value, 100)
38+
}
39+
40+
func testReset() {
41+
let atom = TestValueAtom(value: 0)
42+
let dependency = TestStateAtom(defaultValue: 0)
43+
let store = Store()
44+
let transaction = Transaction(key: AtomKey(atom)) {}
45+
let storeContext = StoreContext(store)
46+
let context = AtomTransactionContext(store: StoreContext(store), transaction: transaction, coordinator: ())
47+
48+
XCTAssertEqual(storeContext.watch(dependency, in: transaction), 0)
49+
50+
context[dependency] = 100
51+
52+
XCTAssertEqual(storeContext.watch(dependency, in: transaction), 100)
53+
54+
context.reset(dependency)
55+
56+
XCTAssertEqual(context.read(dependency), 0)
57+
}
58+
}

Tests/AtomsTests/Utilities/TestAtom.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ struct TestValueAtom<T>: ValueAtom {
2121
value
2222
}
2323

24-
func updated(newValue: T, oldValue: T, reader: Reader) {
24+
func updated(newValue: T, oldValue: T, context: UpdatedContext) {
2525
onUpdated?(newValue, oldValue)
2626
}
2727
}
@@ -38,7 +38,7 @@ struct TestStateAtom<T>: StateAtom {
3838
defaultValue
3939
}
4040

41-
func updated(newValue: T, oldValue: T, reader: Reader) {
41+
func updated(newValue: T, oldValue: T, context: UpdatedContext) {
4242
onUpdated?(newValue, oldValue)
4343
}
4444
}
@@ -70,7 +70,7 @@ struct TestTaskAtom<T: Sendable>: TaskAtom {
7070
func updated(
7171
newValue: Task<T, Never>,
7272
oldValue: Task<T, Never>,
73-
reader: Reader
73+
context: UpdatedContext
7474
) {
7575
onUpdated?(newValue, oldValue)
7676
}
@@ -103,7 +103,7 @@ struct TestThrowingTaskAtom<Success: Sendable>: ThrowingTaskAtom {
103103
func updated(
104104
newValue: Task<Success, Error>,
105105
oldValue: Task<Success, Error>,
106-
reader: Reader
106+
context: UpdatedContext
107107
) {
108108
onUpdated?(newValue, oldValue)
109109
}
@@ -124,7 +124,7 @@ struct TestPublisherAtom<Publisher: Combine.Publisher>: PublisherAtom {
124124
func updated(
125125
newValue: AsyncPhase<Publisher.Output, Publisher.Failure>,
126126
oldValue: AsyncPhase<Publisher.Output, Publisher.Failure>,
127-
reader: Reader
127+
context: UpdatedContext
128128
) {
129129
onUpdated?(newValue, oldValue)
130130
}
@@ -145,7 +145,7 @@ struct TestAsyncSequenceAtom<Sequence: AsyncSequence>: AsyncSequenceAtom {
145145
func updated(
146146
newValue: AsyncPhase<Sequence.Element, Error>,
147147
oldValue: AsyncPhase<Sequence.Element, Error>,
148-
reader: Reader
148+
context: UpdatedContext
149149
) {
150150
onUpdated?(newValue, oldValue)
151151
}

0 commit comments

Comments
 (0)