Skip to content

Commit bb45147

Browse files
authored
Move restore function to ViewContext (#66)
* Remove restore function from Snapshot itself and move it to AtomViewContext * Fix example app * Update README
1 parent 55bd331 commit bb45147

File tree

7 files changed

+76
-98
lines changed

7 files changed

+76
-98
lines changed

Examples/Packages/iOS/Sources/ExampleTimeTravel/ExampleTimeTravel.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ struct TimeTravelDebug<Content: View>: View {
7878
@State
7979
var position = 0
8080

81+
@ViewContext
82+
var context
83+
8184
var body: some View {
8285
AtomScope {
8386
ZStack(alignment: .bottom) {
@@ -105,7 +108,7 @@ struct TimeTravelDebug<Content: View>: View {
105108
set: { value in
106109
Task { @MainActor in
107110
position = Int(value)
108-
snapshots[position].restore()
111+
context.restore(snapshots[position])
109112
}
110113
}
111114
),

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1302,9 +1302,30 @@ AtomRoot {
13021302
}
13031303
```
13041304

1305-
Calling the [restore()](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/snapshot/restore()) method of the obtained `Snapshot` will roll back to the states and dependency graph at that point in time to see what happened.
1305+
`@ViewContext` also supports restoring the values of atoms and the dependency graph captured at a point in time in a retrieved snapshot and its dependency graph so that you can investigate what happend.
13061306
The debugging technique is called [time travel debugging](https://en.wikipedia.org/wiki/Time_travel_debugging), and the example application [here](Examples/Packages/iOS/Sources/ExampleTimeTravel) demonstrates how it works.
13071307

1308+
```swift
1309+
@ViewContext
1310+
var context
1311+
1312+
@State
1313+
var snapshot: Snapshot?
1314+
1315+
var body: some View {
1316+
VStack {
1317+
Button("Capture") {
1318+
snapshot = context.snapshot()
1319+
}
1320+
Button("Restore") {
1321+
if let snapshot {
1322+
context.restore(snapshot)
1323+
}
1324+
}
1325+
}
1326+
}
1327+
```
1328+
13081329
In addition, [graphDescription()](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/snapshot/graphdescription()) method returns a string, that represents the dependencies graph and where they are used, as a String in [graph description language DOT](https://graphviz.org/doc/info/lang.html).
13091330
This can be converted to an image using [Graphviz](https://graphviz.org), a graph visualization tool, to visually analyze information about the state of the application, as shown below.
13101331

Sources/Atoms/Context/AtomViewContext.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,14 @@ public struct AtomViewContext: AtomWatchableContext {
170170
public func snapshot() -> Snapshot {
171171
_store.snapshot()
172172
}
173+
174+
/// For debugging, restore atom values and the dependency graph captured at a point in time in the given snapshot.
175+
///
176+
/// Atoms and their dependencies that are no longer subscribed to from anywhere are then released.
177+
///
178+
/// - Parameter snapshot: A snapshot that captures specific set of values of atoms.
179+
@inlinable
180+
public func restore(_ snapshot: Snapshot) {
181+
_store.restore(snapshot)
182+
}
173183
}

Sources/Atoms/Core/StoreContext.swift

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -191,44 +191,44 @@ internal struct StoreContext {
191191
@usableFromInline
192192
func snapshot() -> Snapshot {
193193
let store = getStore()
194-
let graph = store.graph
195-
let caches = store.state.caches
196-
let subscriptions = store.state.subscriptions
197194

198195
return Snapshot(
199-
graph: graph,
200-
caches: caches,
201-
subscriptions: subscriptions
202-
) {
203-
let store = getStore()
204-
let keys = ContiguousArray(caches.keys)
205-
var obsoletedDependencies = [AtomKey: Set<AtomKey>]()
206-
207-
for key in keys {
208-
let oldDependencies = store.graph.dependencies[key]
209-
let newDependencies = graph.dependencies[key]
210-
211-
// Update atom values and the graph.
212-
store.state.caches[key] = caches[key]
213-
store.graph.dependencies[key] = newDependencies
214-
store.graph.children[key] = graph.children[key]
215-
obsoletedDependencies[key] = oldDependencies?.subtracting(newDependencies ?? [])
216-
}
196+
graph: store.graph,
197+
caches: store.state.caches,
198+
subscriptions: store.state.subscriptions
199+
)
200+
}
217201

218-
for key in keys {
219-
// Release if the atom is no longer used.
220-
checkRelease(for: key)
202+
@usableFromInline
203+
func restore(_ snapshot: Snapshot) {
204+
let store = getStore()
205+
let keys = ContiguousArray(snapshot.caches.keys)
206+
var obsoletedDependencies = [AtomKey: Set<AtomKey>]()
221207

222-
// Release dependencies that are no longer dependent.
223-
if let dependencies = obsoletedDependencies[key] {
224-
checkReleaseDependencies(dependencies, for: key)
225-
}
208+
for key in keys {
209+
let oldDependencies = store.graph.dependencies[key]
210+
let newDependencies = snapshot.graph.dependencies[key]
226211

227-
// Notify updates only for the subscriptions of restored atoms.
228-
if let subscriptions = store.state.subscriptions[key] {
229-
for subscription in ContiguousArray(subscriptions.values) {
230-
subscription.notifyUpdate()
231-
}
212+
// Update atom values and the graph.
213+
store.state.caches[key] = snapshot.caches[key]
214+
store.graph.dependencies[key] = newDependencies
215+
store.graph.children[key] = snapshot.graph.children[key]
216+
obsoletedDependencies[key] = oldDependencies?.subtracting(newDependencies ?? [])
217+
}
218+
219+
for key in keys {
220+
// Release if the atom is no longer used.
221+
checkRelease(for: key)
222+
223+
// Release dependencies that are no longer dependent.
224+
if let dependencies = obsoletedDependencies[key] {
225+
checkReleaseDependencies(dependencies, for: key)
226+
}
227+
228+
// Notify updates only for the subscriptions of restored atoms.
229+
if let subscriptions = store.state.subscriptions[key] {
230+
for subscription in ContiguousArray(subscriptions.values) {
231+
subscription.notifyUpdate()
232232
}
233233
}
234234
}

Sources/Atoms/Snapshot.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,15 @@ public struct Snapshot: CustomStringConvertible {
33
internal let graph: Graph
44
internal let caches: [AtomKey: any AtomCacheProtocol]
55
internal let subscriptions: [AtomKey: [SubscriptionKey: Subscription]]
6-
private let _restore: @MainActor () -> Void
76

87
internal init(
98
graph: Graph,
109
caches: [AtomKey: any AtomCacheProtocol],
11-
subscriptions: [AtomKey: [SubscriptionKey: Subscription]],
12-
restore: @MainActor @escaping () -> Void
10+
subscriptions: [AtomKey: [SubscriptionKey: Subscription]]
1311
) {
1412
self.graph = graph
1513
self.caches = caches
1614
self.subscriptions = subscriptions
17-
self._restore = restore
1815
}
1916

2017
/// A textual representation of this snapshot.
@@ -26,12 +23,6 @@ public struct Snapshot: CustomStringConvertible {
2623
"""
2724
}
2825

29-
/// Restores the atom values that are captured in this snapshot.
30-
@MainActor
31-
public func restore() {
32-
_restore()
33-
}
34-
3526
/// Lookup a value associated with the given atom from the set captured in this snapshot..
3627
///
3728
/// Note that this does not look up a overridden atom.

Tests/AtomsTests/Core/StoreContextTests.swift

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -284,19 +284,15 @@ final class StoreContextTests: XCTestCase {
284284
XCTAssertEqual(context.read(atom), 0)
285285
}
286286

287-
func testSnapshot() {
287+
func testSnapshotAndRestore() {
288288
let store = AtomStore()
289289
let token = SubscriptionKey.Token()
290290
let subscriptionKey = SubscriptionKey(token: token)
291291
let context = StoreContext(store)
292292
let atom0 = TestAtom(value: 0)
293293
let atom1 = TestAtom(value: 1)
294-
let atom2 = TestAtom(value: 2)
295-
let atom3 = TestAtom(value: 3)
296294
let key0 = AtomKey(atom0)
297295
let key1 = AtomKey(atom1)
298-
let key2 = AtomKey(atom2)
299-
let key3 = AtomKey(atom3)
300296
let graph = Graph(
301297
dependencies: [key0: [key1]],
302298
children: [key1: [key0]]
@@ -319,34 +315,6 @@ final class StoreContextTests: XCTestCase {
319315
snapshot.caches.mapValues { $0 as? AtomCache<TestAtom<Int>> },
320316
caches
321317
)
322-
323-
// Modify graph and caches to a form that atom2 & atom3 should be released.
324-
store.graph = Graph(
325-
dependencies: [key0: [key1, key2], key2: [key3]],
326-
children: [key1: [key0], key2: [key0], key3: [key2]]
327-
)
328-
store.state.caches = [
329-
key0: AtomCache(atom: atom0, value: 0),
330-
key1: AtomCache(atom: atom1, value: 1),
331-
key2: AtomCache(atom: atom2, value: 2),
332-
key3: AtomCache(atom: atom3, value: 3),
333-
]
334-
335-
snapshot.restore()
336-
337-
XCTAssertEqual(store.graph, snapshot.graph)
338-
XCTAssertEqual(
339-
store.state.caches.mapValues { $0 as? AtomCache<TestAtom<Int>> },
340-
snapshot.caches.mapValues { $0 as? AtomCache<TestAtom<Int>> }
341-
)
342-
343-
// Remove all subscriptions so all atoms should be released.
344-
store.state.subscriptions = [:]
345-
346-
snapshot.restore()
347-
348-
XCTAssertEqual(store.graph, Graph())
349-
XCTAssertTrue(store.state.caches.isEmpty)
350318
}
351319

352320
func testScopedOverride() async {
@@ -898,7 +866,7 @@ final class StoreContextTests: XCTestCase {
898866
AtomKey(atom2): [subscriptionKey: subscription2],
899867
]
900868

901-
snapshot.restore()
869+
context.restore(snapshot)
902870

903871
// Notifies updated only for the subscriptions of the atoms that are restored.
904872
XCTAssertEqual(updated, [AtomKey(atom0), AtomKey(atom1)])
@@ -921,7 +889,7 @@ final class StoreContextTests: XCTestCase {
921889

922890
// Restore with no subscriptions.
923891
store.state.subscriptions.removeAll()
924-
snapshot.restore()
892+
context.restore(snapshot)
925893

926894
XCTAssertEqual(store.graph, Graph())
927895

Tests/AtomsTests/SnapshotTests.swift

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,6 @@ import XCTest
44

55
@MainActor
66
final class SnapshotTests: XCTestCase {
7-
func testRestore() {
8-
var isRestoreCalled = false
9-
let snapshot = Snapshot(
10-
graph: Graph(),
11-
caches: [:],
12-
subscriptions: [:]
13-
) {
14-
isRestoreCalled = true
15-
}
16-
17-
snapshot.restore()
18-
19-
XCTAssertTrue(isRestoreCalled)
20-
}
21-
227
func testLookup() {
238
let atom0 = TestAtom(value: 0)
249
let atom1 = TestAtom(value: 1)
@@ -29,7 +14,7 @@ final class SnapshotTests: XCTestCase {
2914
graph: Graph(),
3015
caches: atomCache,
3116
subscriptions: [:]
32-
) {}
17+
)
3318

3419
XCTAssertEqual(snapshot.lookup(atom0), 0)
3520
XCTAssertNil(snapshot.lookup(atom1))
@@ -40,7 +25,7 @@ final class SnapshotTests: XCTestCase {
4025
graph: Graph(),
4126
caches: [:],
4227
subscriptions: [:]
43-
) {}
28+
)
4429

4530
XCTAssertEqual(snapshot.graphDescription(), "digraph {}")
4631
}
@@ -93,7 +78,7 @@ final class SnapshotTests: XCTestCase {
9378
key3: [subscriber: subscription],
9479
key4: [subscriber: subscription],
9580
]
96-
) {}
81+
)
9782

9883
XCTAssertEqual(
9984
snapshot.graphDescription(),

0 commit comments

Comments
 (0)