Skip to content

Commit b3dd595

Browse files
authored
Modify accessor (#60)
* Add modify accessor to contexts * Add tests * Use modify in example app * Update README
1 parent 135b96c commit b3dd595

File tree

9 files changed

+208
-16
lines changed

9 files changed

+208
-16
lines changed

Examples/Packages/CrossPlatform/Sources/ExampleTodo/Screens.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ struct TodoListScreen: View {
2222
TodoItem(todo: todo)
2323
}
2424
.onDelete { indexSet in
25-
let todos = TodosAtom()
26-
let indices = indexSet.compactMap { index in
27-
context[todos].firstIndex(of: filteredTodos[index])
25+
let filtered = filteredTodos
26+
context.modify(TodosAtom()) { todos in
27+
let indices = indexSet.compactMap { index in
28+
todos.firstIndex(of: filtered[index])
29+
}
30+
todos.remove(atOffsets: IndexSet(indices))
2831
}
29-
context[todos].remove(atOffsets: IndexSet(indices))
3032
}
3133
}
3234
}

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -902,13 +902,14 @@ Context is a structure for using and interacting with atom values from views or
902902

903903
|API|Use|
904904
|:--|:--|
905-
|[watch(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomwatchablecontext/watch(_:))|Obtains an atom value and starts watching its update.|
906-
|[read(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/read(_:))|Obtains an atom value but does not watch its update.|
907-
|[set(_:for:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/set(_:for:))|Sets a new value to the atom.|
908-
|[[:_]](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/subscript(_:)) subscript|Read-write access for applying mutating methods.|
909-
|[state(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomwatchablecontext/state(_:))|Gets a binding to the atom state.|
910-
|[refresh(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/refresh(_:))|Reset an atom and await until asynchronous operation is complete.|
911-
|[reset(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/reset(_:))|Reset an atom to the default value or a first output.|
905+
|[watch](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomwatchablecontext/watch(_:))|Obtains an atom value and starts watching its update.|
906+
|[read](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/read(_:))|Obtains an atom value but does not watch its update.|
907+
|[set](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/set(_:for:))|Sets a new value to the atom.|
908+
|[modify](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/modify(_:body:))|Modifies the cached atom value.|
909+
|[subscript[]](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/subscript(_:))|Read-write access for applying mutating methods.|
910+
|[state](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomwatchablecontext/state(_:))|Gets a binding to the atom state.|
911+
|[refresh](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/refresh(_:))|Reset an atom and await until asynchronous operation is complete.|
912+
|[reset](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomcontext/reset(_:))|Reset an atom to the default value or a first output.|
912913

913914
There are the following types context as different contextual environments.
914915
The APIs described in each section below are their own specific functionality depending on the environment in which it is used, in addition to the above common APIs.

Sources/Atoms/Context/AtomContext.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@ public protocol AtomContext {
4141
/// - atom: An atom that associates the value.
4242
func set<Node: StateAtom>(_ value: Node.Loader.Value, for atom: Node)
4343

44+
/// Modifies the cached value of the given writable atom.
45+
///
46+
/// This method only accepts writable atoms such as types conforming to ``StateAtom``,
47+
/// and assign a new value for the atom.
48+
/// When you modify value, it notifies update to downstream atoms or views after all
49+
/// the modification completed.
50+
///
51+
/// ```swift
52+
/// let context = ...
53+
/// print(context.watch(TextAtom())) // Prints "Text"
54+
/// context.modify(TextAtom()) { text in
55+
/// text.append(" modified")
56+
/// }
57+
/// print(context.read(TextAtom())) // Prints "Text modified"
58+
/// ```
59+
///
60+
/// - Parameters
61+
/// - atom: An atom that associates the value.
62+
/// - body: A value modification body.
63+
func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void)
64+
4465
/// Refreshes and then return the value associated with the given refreshable atom.
4566
///
4667
/// This method only accepts refreshable atoms such as types conforming to:

Sources/Atoms/Context/AtomTestContext.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,29 @@ public struct AtomTestContext: AtomWatchableContext {
129129
store.set(value, for: atom)
130130
}
131131

132+
/// Modifies the cached value of the given writable atom.
133+
///
134+
/// This method only accepts writable atoms such as types conforming to ``StateAtom``,
135+
/// and assign a new value for the atom.
136+
/// When you modify value, it notifies update to downstream atoms or views after all
137+
/// the modification completed.
138+
///
139+
/// ```swift
140+
/// let context = ...
141+
/// print(context.watch(TextAtom())) // Prints "Text"
142+
/// context.modify(TextAtom()) { text in
143+
/// text.append(" modified")
144+
/// }
145+
/// print(context.read(TextAtom())) // Prints "Text modified"
146+
/// ```
147+
///
148+
/// - Parameters
149+
/// - atom: An atom that associates the value.
150+
/// - body: A value modification body.
151+
public func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
152+
store.modify(atom, body: body)
153+
}
154+
132155
/// Refreshes and then return the value associated with the given refreshable atom.
133156
///
134157
/// This method only accepts refreshable atoms such as types conforming to:

Sources/Atoms/Context/AtomTransactionContext.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,30 @@ public struct AtomTransactionContext<Coordinator>: AtomWatchableContext {
6464
_store.set(value, for: atom)
6565
}
6666

67+
/// Modifies the cached value of the given writable atom.
68+
///
69+
/// This method only accepts writable atoms such as types conforming to ``StateAtom``,
70+
/// and assign a new value for the atom.
71+
/// When you modify value, it notifies update to downstream atoms or views after all
72+
/// the modification completed.
73+
///
74+
/// ```swift
75+
/// let context = ...
76+
/// print(context.watch(TextAtom())) // Prints "Text"
77+
/// context.modify(TextAtom()) { text in
78+
/// text.append(" modified")
79+
/// }
80+
/// print(context.read(TextAtom())) // Prints "Text modified"
81+
/// ```
82+
///
83+
/// - Parameters
84+
/// - atom: An atom that associates the value.
85+
/// - body: A value modification body.
86+
@inlinable
87+
public func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
88+
_store.modify(atom, body: body)
89+
}
90+
6791
/// Refreshes and then return the value associated with the given refreshable atom.
6892
///
6993
/// This method only accepts refreshable atoms such as types conforming to:

Sources/Atoms/Context/AtomUpdatedContext.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,30 @@ public struct AtomUpdatedContext<Coordinator>: AtomContext {
5656
_store.set(value, for: atom)
5757
}
5858

59+
/// Modifies the cached value of the given writable atom.
60+
///
61+
/// This method only accepts writable atoms such as types conforming to ``StateAtom``,
62+
/// and assign a new value for the atom.
63+
/// When you modify value, it notifies update to downstream atoms or views after all
64+
/// the modification completed.
65+
///
66+
/// ```swift
67+
/// let context = ...
68+
/// print(context.watch(TextAtom())) // Prints "Text"
69+
/// context.modify(TextAtom()) { text in
70+
/// text.append(" modified")
71+
/// }
72+
/// print(context.read(TextAtom())) // Prints "Text modified"
73+
/// ```
74+
///
75+
/// - Parameters
76+
/// - atom: An atom that associates the value.
77+
/// - body: A value modification body.
78+
@inlinable
79+
public func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
80+
_store.modify(atom, body: body)
81+
}
82+
5983
/// Refreshes and then return the value associated with the given refreshable atom.
6084
///
6185
/// This method only accepts refreshable atoms such as types conforming to:

Sources/Atoms/Context/AtomViewContext.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,30 @@ public struct AtomViewContext: AtomWatchableContext {
6363
_store.set(value, for: atom)
6464
}
6565

66+
/// Modifies the cached value of the given writable atom.
67+
///
68+
/// This method only accepts writable atoms such as types conforming to ``StateAtom``,
69+
/// and assign a new value for the atom.
70+
/// When you modify value, it notifies update to downstream atoms or views after all
71+
/// the modification completed.
72+
///
73+
/// ```swift
74+
/// let context = ...
75+
/// print(context.watch(TextAtom())) // Prints "Text"
76+
/// context.modify(TextAtom()) { text in
77+
/// text.append(" modified")
78+
/// }
79+
/// print(context.read(TextAtom())) // Prints "Text modified"
80+
/// ```
81+
///
82+
/// - Parameters
83+
/// - atom: An atom that associates the value.
84+
/// - body: A value modification body.
85+
@inlinable
86+
public func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
87+
_store.modify(atom, body: body)
88+
}
89+
6690
/// Refreshes and then return the value associated with the given refreshable atom.
6791
///
6892
/// This method only accepts refreshable atoms such as types conforming to:

Sources/Atoms/Core/StoreContext.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ internal struct StoreContext {
4141
set(value, for: atom, scope: current)
4242
}
4343

44+
@usableFromInline
45+
func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
46+
modify(atom, scope: current, body: body)
47+
}
48+
4449
@usableFromInline
4550
func watch<Node: Atom>(_ atom: Node, in transaction: Transaction) -> Node.Loader.Value {
4651
watch(atom, in: transaction, scope: current)
@@ -127,6 +132,24 @@ private extension StoreContext {
127132
}
128133
}
129134

135+
func modify<Node: StateAtom>(_ atom: Node, scope: Scope, body: (inout Node.Loader.Value) -> Void) {
136+
let key = AtomKey(atom)
137+
138+
if let cache = peekCache(of: atom, for: key, scope: scope) {
139+
var value = cache.value
140+
body(&value)
141+
update(atom: atom, for: key, value: value, cache: cache, scope: scope)
142+
checkRelease(for: key, scope: scope)
143+
}
144+
else if scope.overrides.hasValue(for: key) {
145+
// Do nothing if the atom is overridden.
146+
return
147+
}
148+
else if let parent = scope.parent {
149+
return modify(atom, scope: parent, body: body)
150+
}
151+
}
152+
130153
func watch<Node: Atom>(_ atom: Node, in transaction: Transaction, scope: Scope) -> Node.Loader.Value {
131154
guard !transaction.isTerminated else {
132155
return read(atom)

Tests/AtomsTests/Core/StoreContextTests.swift

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,42 @@ final class StoreContextTests: XCTestCase {
6666
XCTAssertEqual((store.state.caches[key] as? AtomCache<TestStateAtom<Int>>)?.value, 3)
6767
}
6868

69+
func testModify() {
70+
let store = AtomStore()
71+
let context = StoreContext(store)
72+
let subscriptionKey = SubscriptionKey(SubscriptionContainer())
73+
var updateCount = 0
74+
let atom = TestStateAtom(defaultValue: 0)
75+
let key = AtomKey(atom)
76+
77+
context.set(1, for: atom)
78+
context.modify(atom) { $0 = 1 }
79+
80+
XCTAssertEqual(updateCount, 0)
81+
XCTAssertNil(store.state.states[key])
82+
XCTAssertNil(store.state.caches[key])
83+
84+
store.state.caches[key] = AtomCache(atom: atom, value: 0)
85+
store.state.states[key] = AtomState(coordinator: atom.makeCoordinator())
86+
store.state.subscriptions[key, default: [:]][subscriptionKey] = Subscription(
87+
notifyUpdate: { updateCount += 1 },
88+
unsubscribe: {}
89+
)
90+
91+
context.modify(atom) { $0 = 2 }
92+
93+
XCTAssertEqual(updateCount, 1)
94+
XCTAssertNil(store.state.states[key]?.transaction)
95+
XCTAssertEqual((store.state.caches[key] as? AtomCache<TestStateAtom<Int>>)?.value, 2)
96+
97+
context.modify(atom) { $0 = 3 }
98+
99+
XCTAssertEqual(updateCount, 2)
100+
XCTAssertNotNil(store.state.states[key])
101+
XCTAssertNil(store.state.states[key]?.transaction)
102+
XCTAssertEqual((store.state.caches[key] as? AtomCache<TestStateAtom<Int>>)?.value, 3)
103+
}
104+
69105
func testWatch() {
70106
let store = AtomStore()
71107
let context = StoreContext(store)
@@ -249,7 +285,7 @@ final class StoreContextTests: XCTestCase {
249285
XCTAssertTrue(store.state.caches.isEmpty)
250286
}
251287

252-
func testScopedObservers() {
288+
func testScoped() {
253289
let store = AtomStore()
254290
let container = SubscriptionContainer()
255291
let atom = TestValueAtom(value: 0)
@@ -340,15 +376,17 @@ final class StoreContextTests: XCTestCase {
340376

341377
XCTAssertEqual(context.watch(dependency1Atom, container: container.wrapper) {}, 0)
342378

379+
// Shouldn't set value if the atom is overridden in the scope.
343380
scoped1Context.set(1, for: dependency1Atom)
381+
XCTAssertEqual(context.read(dependency1Atom), 0)
344382

345-
// Shouldn't set value if the atom is overridden in the scope.
383+
// Shouldn't modify value if the atom is overridden in the scope.
384+
scoped1Context.modify(dependency1Atom) { $0 = 1 }
346385
XCTAssertEqual(context.read(dependency1Atom), 0)
347386

387+
// Shouldn't reset value if the atom is overridden in the scope.
348388
context.set(1, for: dependency1Atom)
349389
scoped1Context.reset(dependency1Atom)
350-
351-
// Shouldn't reset value if the atom is overridden in the scope.
352390
XCTAssertEqual(context.read(dependency1Atom), 1)
353391

354392
container.wrapper.subscriptions[AtomKey(dependency1Atom)]?.unsubscribe()
@@ -373,9 +411,21 @@ final class StoreContextTests: XCTestCase {
373411
XCTAssertEqual(scoped2Context.read(dependency2Atom), 100)
374412
XCTAssertEqual(context.read(atom), 120)
375413

414+
// Should modify the value and then propagate it to the dependent atoms..
415+
scoped2Context.modify(dependency1Atom) { $0 = 40 }
416+
417+
// Shouldn't modify the value here because the atom is cached in the scoped store.
418+
context.modify(dependency1Atom) { $0 = 50 }
419+
420+
// Should return overridden values.
421+
XCTAssertEqual(scoped2Context.read(atom), 140)
422+
XCTAssertEqual(scoped2Context.read(dependency1Atom), 40)
423+
XCTAssertEqual(scoped2Context.read(dependency2Atom), 100)
424+
XCTAssertEqual(context.read(atom), 140)
425+
376426
do {
377427
let phase = await scoped2Context.refresh(publisherAtom)
378-
XCTAssertEqual(phase, .success(120))
428+
XCTAssertEqual(phase, .success(140))
379429
}
380430

381431
// Should reset the value and then propagate it to the dependent atoms..

0 commit comments

Comments
 (0)