Skip to content

Commit 657d1cf

Browse files
authored
Feature: Custom Resettable Attribute (#98)
* Documentation: Fix subscript(_:) references * Custom Resettable Attribute * Add Resettable to README * Fix linter warnings * Fix Missing _disfavoredOverload * Arbitrary reset * Reflect on tests * Reflect documentation * Merge upstream and sync * Cleanup Dead code * Format
1 parent 5eeec4f commit 657d1cf

17 files changed

+386
-27
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,38 @@ struct FetchMoviesPhaseAtom: ValueAtom, Refreshable, Hashable {
805805
}
806806
```
807807

808+
809+
#### [Resettable](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/resettable)
810+
811+
`Resettable` allows you to implement a custom reset behavior to an atom.
812+
813+
<details><summary><code>📖 Expand to see example</code></summary>
814+
815+
It adds custom reset behavior to an Atom that will be executed upon atom reset.
816+
817+
It's useful when need to have arbitrary reset ability or implementing reset when value depends on private atom.
818+
819+
In following example, `RandomIntAtom` generates a random value using generated from private `RandomNumberGeneratorAtom`, and `Resettable` gives ability to replace exposed reset with `RandomNumberGeneratorAtom` reset.
820+
821+
```swift
822+
struct RandomIntAtom: ValueAtom, Resettable, Hashable {
823+
func value(context: Context) -> Int {
824+
var generator = context.watch(RandomNumberGeneratorAtom())
825+
return .random(in: 0..<100, using: &generator)
826+
}
827+
828+
func reset(context: ResetContext) {
829+
context.reset(RandomNumberGeneratorAtom())
830+
}
831+
}
832+
833+
private struct RandomNumberGeneratorAtom: ValueAtom, Hashable {
834+
func value(context: Context) -> CustomRandomNumberGenerator {
835+
CustomRandomNumberGenerator()
836+
}
837+
}
838+
```
839+
808840
</details>
809841

810842
---

Sources/Atoms/Atoms.docc/Atoms.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Building state by compositing atoms automatically optimizes rendering based on i
3535

3636
- ``KeepAlive``
3737
- ``Refreshable``
38+
- ``Resettable``
3839

3940
### Property Wrappers
4041

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/// An attribute protocol allows an atom to have a custom reset override.
2+
///
3+
/// Note that the custom reset will be triggered even when the atom is overridden.
4+
///
5+
/// ```swift
6+
/// struct UserAtom: ValueAtom, Resettable, Hashable {
7+
/// func value(context: Context) -> User? {
8+
/// context.watch(FetchUserAtom()).phase.value
9+
/// }
10+
///
11+
/// func reset(context: ResetContext) {
12+
/// context.reset(FetchUserAtom())
13+
/// }
14+
/// }
15+
///
16+
/// private struct FetchUserAtom: TaskAtom, Hashable {
17+
/// func value(context: Context) async -> User? {
18+
/// await fetchUser()
19+
/// }
20+
/// }
21+
/// ```
22+
///
23+
public protocol Resettable where Self: Atom {
24+
/// A type of the context structure to read, set, and otherwise interact
25+
/// with other atoms.
26+
typealias ResetContext = AtomCurrentContext<Loader.Coordinator>
27+
28+
/// Arbitrary reset method to be executed on atom reset.
29+
///
30+
/// This is arbitrary custom reset method that replaces regular atom reset functionality.
31+
///
32+
/// - Parameter context: A context structure to read, set, and otherwise interact
33+
/// with other atoms.
34+
@MainActor
35+
func reset(context: ResetContext)
36+
}

Sources/Atoms/Context/AtomContext.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public protocol AtomContext {
2727
/// and assigns a new value for the atom.
2828
/// When you assign a new value, it immediately notifies downstream atoms and views.
2929
///
30-
/// - SeeAlso: ``AtomContext/subscript``
30+
/// - SeeAlso: ``AtomContext/subscript(_:)``
3131
///
3232
/// ```swift
3333
/// let context = ...
@@ -102,7 +102,25 @@ public protocol AtomContext {
102102
/// ```
103103
///
104104
/// - Parameter atom: An atom to reset.
105-
func reset(_ atom: some Atom)
105+
@_disfavoredOverload
106+
func reset<Node: Atom>(_ atom: Node)
107+
108+
/// Calls arbitrary reset function of the given atom.
109+
///
110+
/// This method only accepts atoms that conform to ``Resettable`` protocol.
111+
/// Calls custom reset function of the given atom. Hence, it does not generate any new cache value or notify subscribers.
112+
///
113+
/// ```swift
114+
/// let context = ...
115+
/// print(context.watch(ResettableTextAtom()) // Prints "Text"
116+
/// context[ResettableTextAtom()] = "New text"
117+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
118+
/// context.reset(ResettableTextAtom()) // Calls the custom reset function
119+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
120+
/// ```
121+
///
122+
/// - Parameter atom: An atom to reset.
123+
func reset<Node: Resettable>(_ atom: Node)
106124
}
107125

108126
public extension AtomContext {

Sources/Atoms/Context/AtomCurrentContext.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public struct AtomCurrentContext<Coordinator>: AtomContext {
4040
/// and assigns a new value for the atom.
4141
/// When you assign a new value, it immediately notifies downstream atoms and views.
4242
///
43-
/// - SeeAlso: ``AtomViewContext/subscript``
43+
/// - SeeAlso: ``AtomViewContext/subscript(_:)``
4444
///
4545
/// ```swift
4646
/// let context = ...
@@ -142,7 +142,28 @@ public struct AtomCurrentContext<Coordinator>: AtomContext {
142142
///
143143
/// - Parameter atom: An atom to reset.
144144
@inlinable
145-
public func reset(_ atom: some Atom) {
145+
@_disfavoredOverload
146+
public func reset<Node: Atom>(_ atom: Node) {
147+
_store.reset(atom)
148+
}
149+
150+
/// Calls arbitrary reset function of the given atom.
151+
///
152+
/// This method only accepts atoms that conform to ``Resettable`` protocol.
153+
/// Calls custom reset function of the given atom. Hence, it does not generate any new cache value or notify subscribers.
154+
///
155+
/// ```swift
156+
/// let context = ...
157+
/// print(context.watch(ResettableTextAtom()) // Prints "Text"
158+
/// context[ResettableTextAtom()] = "New text"
159+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
160+
/// context.reset(ResettableTextAtom()) // Calls the custom reset function
161+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
162+
/// ```
163+
///
164+
/// - Parameter atom: An atom to reset.
165+
@inlinable
166+
public func reset<Node: Resettable>(_ atom: Node) {
146167
_store.reset(atom)
147168
}
148169
}

Sources/Atoms/Context/AtomTestContext.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public struct AtomTestContext: AtomWatchableContext {
173173
/// and assigns a new value for the atom.
174174
/// When you assign a new value, it immediately notifies downstream atoms and views.
175175
///
176-
/// - SeeAlso: ``AtomTestContext/subscript``
176+
/// - SeeAlso: ``AtomTestContext/subscript(_:)``
177177
///
178178
/// ```swift
179179
/// let context = AtomTestContext()
@@ -277,7 +277,28 @@ public struct AtomTestContext: AtomWatchableContext {
277277
///
278278
/// - Parameter atom: An atom to reset.
279279
@inlinable
280-
public func reset(_ atom: some Atom) {
280+
@_disfavoredOverload
281+
public func reset<Node: Atom>(_ atom: Node) {
282+
_store.reset(atom)
283+
}
284+
285+
/// Calls arbitrary reset function of the given atom.
286+
///
287+
/// This method only accepts atoms that conform to ``Resettable`` protocol.
288+
/// Calls custom reset function of the given atom. Hence, it does not generate any new cache value or notify subscribers.
289+
///
290+
/// ```swift
291+
/// let context = ...
292+
/// print(context.watch(ResettableTextAtom()) // Prints "Text"
293+
/// context[ResettableTextAtom()] = "New text"
294+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
295+
/// context.reset(ResettableTextAtom()) // Calls the custom reset function
296+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
297+
/// ```
298+
///
299+
/// - Parameter atom: An atom to reset.
300+
@inlinable
301+
public func reset<Node: Resettable>(_ atom: Node) {
281302
_store.reset(atom)
282303
}
283304

Sources/Atoms/Context/AtomTransactionContext.swift

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public struct AtomTransactionContext<Coordinator>: AtomWatchableContext {
4747
/// and assigns a new value for the atom.
4848
/// When you assign a new value, it immediately notifies downstream atoms and views.
4949
///
50-
/// - SeeAlso: ``AtomTransactionContext/subscript``
50+
/// - SeeAlso: ``AtomTransactionContext/subscript(_:)``
5151
///
5252
/// ```swift
5353
/// let context = ...
@@ -142,16 +142,37 @@ public struct AtomTransactionContext<Coordinator>: AtomWatchableContext {
142142
///
143143
/// ```swift
144144
/// let context = ...
145-
/// print(context.watch(TextAtom())) // Prints "Text"
146-
/// context[TextAtom()] = "New text"
147-
/// print(context.read(TextAtom())) // Prints "New text"
148-
/// context.reset(TextAtom())
149-
/// print(context.read(TextAtom())) // Prints "Text"
145+
/// print(context.watch(ResettableTextAtom())) // Prints "Text"
146+
/// context[ResettableTextAtom()] = "New text"
147+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
148+
/// context.reset(ResettableTextAtom())
149+
/// print(context.read(ResettableTextAtom())) // Prints "Text"
150+
/// ```
151+
///
152+
/// - Parameter atom: An atom to reset.
153+
@inlinable
154+
@_disfavoredOverload
155+
public func reset<Node: Atom>(_ atom: Node) {
156+
_store.reset(atom)
157+
}
158+
159+
/// Calls arbitrary reset function of the given atom.
160+
///
161+
/// This method only accepts atoms that conform to ``Resettable`` protocol.
162+
/// Calls custom reset function of the given atom. Hence, it does not generate any new cache value or notify subscribers.
163+
///
164+
/// ```swift
165+
/// let context = ...
166+
/// print(context.watch(ResettableTextAtom()) // Prints "Text"
167+
/// context[ResettableTextAtom()] = "New text"
168+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
169+
/// context.reset(ResettableTextAtom()) // Calls the custom reset function
170+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
150171
/// ```
151172
///
152173
/// - Parameter atom: An atom to reset.
153174
@inlinable
154-
public func reset(_ atom: some Atom) {
175+
public func reset<Node: Resettable>(_ atom: Node) {
155176
_store.reset(atom)
156177
}
157178

Sources/Atoms/Context/AtomViewContext.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public struct AtomViewContext: AtomWatchableContext {
4646
/// and assigns a new value for the atom.
4747
/// When you assign a new value, it immediately notifies downstream atoms and views.
4848
///
49-
/// - SeeAlso: ``AtomViewContext/subscript``
49+
/// - SeeAlso: ``AtomViewContext/subscript(_:)``
5050
///
5151
/// ```swift
5252
/// let context = ...
@@ -150,7 +150,28 @@ public struct AtomViewContext: AtomWatchableContext {
150150
///
151151
/// - Parameter atom: An atom to reset.
152152
@inlinable
153-
public func reset(_ atom: some Atom) {
153+
@_disfavoredOverload
154+
public func reset<Node: Atom>(_ atom: Node) {
155+
_store.reset(atom)
156+
}
157+
158+
/// Calls arbitrary reset function of the given atom.
159+
///
160+
/// This method only accepts atoms that conform to ``Resettable`` protocol.
161+
/// Calls custom reset function of the given atom. Hence, it does not generate any new cache value or notify subscribers.
162+
///
163+
/// ```swift
164+
/// let context = ...
165+
/// print(context.watch(ResettableTextAtom()) // Prints "Text"
166+
/// context[ResettableTextAtom()] = "New text"
167+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
168+
/// context.reset(ResettableTextAtom()) // Calls the custom reset function
169+
/// print(context.read(ResettableTextAtom())) // Prints "New text"
170+
/// ```
171+
///
172+
/// - Parameter atom: An atom to reset.
173+
@inlinable
174+
public func reset<Node: Resettable>(_ atom: Node) {
154175
_store.reset(atom)
155176
}
156177

Sources/Atoms/Core/StoreContext.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ internal struct StoreContext {
201201
}
202202

203203
@usableFromInline
204-
func reset(_ atom: some Atom) {
204+
@_disfavoredOverload
205+
func reset<Node: Atom>(_ atom: Node) {
205206
let override = lookupOverride(of: atom)
206207
let key = AtomKey(atom, overrideScopeKey: override?.scopeKey)
207208

@@ -211,6 +212,15 @@ internal struct StoreContext {
211212
}
212213
}
213214

215+
@usableFromInline
216+
func reset<Node: Resettable>(_ atom: Node) {
217+
let override = lookupOverride(of: atom)
218+
let key = AtomKey(atom, overrideScopeKey: override?.scopeKey)
219+
let state = getState(of: atom, for: key)
220+
let context = AtomCurrentContext(store: self, coordinator: state.coordinator)
221+
atom.reset(context: context)
222+
}
223+
214224
@usableFromInline
215225
func lookup<Node: Atom>(_ atom: Node) -> Node.Loader.Value? {
216226
let override = lookupOverride(of: atom)

Tests/AtomsTests/Atom/TaskAtomTests.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,6 @@ final class TaskAtomTests: XCTestCase {
6666

6767
do {
6868
// Cancellation
69-
var updateCount = 0
70-
context.onUpdate = { updateCount += 1 }
71-
7269
let refreshTask0 = Task {
7370
await context.refresh(atom)
7471
}

0 commit comments

Comments
 (0)