Skip to content

Commit 55bd331

Browse files
authored
Flatten scoped stores (#65)
* Cache overridden atom states into a single store instead of hierarchical multi stores * Add reasonably unique suffix to overridden atoms for graphDesdription * Release atoms that marked as KeepAlive when it is no longer watched * Do not access StateObject that is not installed on a view * Drop support to lookup overridden atoms in Snapshot for consistency * Lint * Refactoring
1 parent 8292e8e commit 55bd331

15 files changed

+632
-546
lines changed

Sources/Atoms/AtomRoot.swift

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,26 @@ public struct AtomRoot<Content: View>: View {
6060
public var body: some View {
6161
content.environment(
6262
\.store,
63-
StoreContext(
63+
.scoped(
6464
state.store,
65-
overrides: overrides,
66-
observers: observers
65+
scopeKey: ScopeKey(token: state.token),
66+
observers: observers,
67+
overrides: overrides
6768
)
6869
)
6970
}
7071

72+
/// For debugging purposes, each time there is a change in the internal state,
73+
/// a snapshot is taken that captures the state of the atoms and their dependency graph
74+
/// at that point in time.
75+
///
76+
/// - Parameter onUpdate: A closure to handle a snapshot of recent updates.
77+
///
78+
/// - Returns: The self instance.
79+
public func observe(_ onUpdate: @escaping @MainActor (Snapshot) -> Void) -> Self {
80+
mutating { $0.observers.append(Observer(onUpdate: onUpdate)) }
81+
}
82+
7183
/// Overrides the atom value with the given value.
7284
///
7385
/// When accessing the overridden atom, this context will create and return the given value
@@ -97,23 +109,13 @@ public struct AtomRoot<Content: View>: View {
97109
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
98110
mutating { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
99111
}
100-
101-
/// For debugging purposes, each time there is a change in the internal state,
102-
/// a snapshot is taken that captures the state of the atoms and their dependency graph
103-
/// at that point in time.
104-
///
105-
/// - Parameter onUpdate: A closure to handle a snapshot of recent updates.
106-
///
107-
/// - Returns: The self instance.
108-
public func observe(_ onUpdate: @escaping @MainActor (Snapshot) -> Void) -> Self {
109-
mutating { $0.observers.append(Observer(onUpdate: onUpdate)) }
110-
}
111112
}
112113

113114
private extension AtomRoot {
114115
@MainActor
115116
final class State: ObservableObject {
116117
let store = AtomStore()
118+
let token = ScopeKey.Token()
117119
}
118120

119121
func `mutating`(_ mutation: (inout Self) -> Void) -> Self {

Sources/Atoms/AtomScope.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,27 @@ public struct AtomScope<Content: View>: View {
101101
content.environment(
102102
\.store,
103103
(store ?? environmentStore).scoped(
104-
store: state.store,
105-
overrides: overrides,
106-
observers: observers
104+
key: ScopeKey(token: state.token),
105+
observers: observers,
106+
overrides: overrides
107107
)
108108
)
109109
}
110110

111+
/// For debugging purposes, each time there is a change in the internal state,
112+
/// a snapshot is taken that captures the state of the atoms and their dependency graph
113+
/// at that point in time.
114+
///
115+
/// Note that unlike observed by ``AtomRoot``, this is triggered only by internal state changes
116+
/// caused by atoms use in this scope.
117+
///
118+
/// - Parameter onUpdate: A closure to handle a snapshot of recent updates.
119+
///
120+
/// - Returns: The self instance.
121+
public func observe(_ onUpdate: @escaping @MainActor (Snapshot) -> Void) -> Self {
122+
mutating { $0.observers.append(Observer(onUpdate: onUpdate)) }
123+
}
124+
111125
/// Override the atom value used in this scope with the given value.
112126
///
113127
/// When accessing the overridden atom, this context will create and return the given value
@@ -141,26 +155,12 @@ public struct AtomScope<Content: View>: View {
141155
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
142156
mutating { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
143157
}
144-
145-
/// For debugging purposes, each time there is a change in the internal state,
146-
/// a snapshot is taken that captures the state of the atoms and their dependency graph
147-
/// at that point in time.
148-
///
149-
/// Note that unlike observed by ``AtomRoot``, this is triggered only by internal state changes
150-
/// caused by atoms use in this scope.
151-
///
152-
/// - Parameter onUpdate: A closure to handle a snapshot of recent updates.
153-
///
154-
/// - Returns: The self instance.
155-
public func observe(_ onUpdate: @escaping @MainActor (Snapshot) -> Void) -> Self {
156-
mutating { $0.observers.append(Observer(onUpdate: onUpdate)) }
157-
}
158158
}
159159

160160
private extension AtomScope {
161161
@MainActor
162162
final class State: ObservableObject {
163-
let store = AtomStore()
163+
let token = ScopeKey.Token()
164164
}
165165

166166
func `mutating`(_ mutation: (inout Self) -> Void) -> Self {

Sources/Atoms/Context/AtomTestContext.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,8 @@ public struct AtomTestContext: AtomWatchableContext {
250250
/// It simulates cases where other atoms or views no longer watches to the atom.
251251
///
252252
/// - Parameter atom: An atom that associates the value.
253-
public func unwatch<Node: Atom>(_ atom: Node) {
254-
let key = AtomKey(atom)
255-
container.subscriptions.removeValue(forKey: key)?.unsubscribe()
253+
public func unwatch(_ atom: some Atom) {
254+
store.unwatch(atom, container: container)
256255
}
257256

258257
/// Overrides the atom value with the given value.
@@ -285,6 +284,7 @@ public struct AtomTestContext: AtomWatchableContext {
285284
private extension AtomTestContext {
286285
final class State {
287286
let store = AtomStore()
287+
let token = ScopeKey.Token()
288288
let container = SubscriptionContainer()
289289
let notifier = PassthroughSubject<Void, Never>()
290290
var overrides = [OverrideKey: any AtomOverrideProtocol]()
@@ -297,7 +297,12 @@ private extension AtomTestContext {
297297
}
298298

299299
var store: StoreContext {
300-
StoreContext(state.store, overrides: state.overrides)
300+
.scoped(
301+
state.store,
302+
scopeKey: ScopeKey(token: state.token),
303+
observers: [],
304+
overrides: state.overrides
305+
)
301306
}
302307

303308
var container: SubscriptionContainer.Wrapper {

Sources/Atoms/Core/AtomKey.swift

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
1-
internal struct AtomKey: Hashable {
1+
internal struct AtomKey: Hashable, CustomStringConvertible {
22
private let key: AnyHashable
33
private let type: ObjectIdentifier
4+
private let overrideScopeKey: ScopeKey?
45
private let getName: () -> String
56

6-
var name: String {
7-
getName()
7+
var isOverridden: Bool {
8+
overrideScopeKey != nil
89
}
910

10-
init<Node: Atom>(_ atom: Node) {
11-
key = AnyHashable(atom.key)
12-
type = ObjectIdentifier(Node.self)
13-
getName = { String(describing: Node.self) }
11+
var description: String {
12+
if let overrideScopeKey {
13+
return getName() + "-override:\(overrideScopeKey.id)"
14+
}
15+
else {
16+
return getName()
17+
}
18+
}
19+
20+
init<Node: Atom>(_ atom: Node, overrideScopeKey: ScopeKey?) {
21+
self.key = AnyHashable(atom.key)
22+
self.type = ObjectIdentifier(Node.self)
23+
self.overrideScopeKey = overrideScopeKey
24+
self.getName = { String(describing: Node.self) }
1425
}
1526

1627
func hash(into hasher: inout Hasher) {
1728
hasher.combine(key)
1829
hasher.combine(type)
30+
hasher.combine(overrideScopeKey)
1931
}
2032

2133
static func == (lhs: Self, rhs: Self) -> Bool {
22-
lhs.key == rhs.key && lhs.type == rhs.type
34+
lhs.key == rhs.key && lhs.type == rhs.type && lhs.overrideScopeKey == rhs.overrideScopeKey
2335
}
2436
}
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
1-
@MainActor
21
internal protocol AtomOverrideProtocol {
32
associatedtype Node: Atom
43

54
var value: (Node) -> Node.Loader.Value { get }
5+
6+
func scoped(key: ScopeKey) -> any AtomScopedOverrideProtocol
67
}
78

89
internal struct AtomOverride<Node: Atom>: AtomOverrideProtocol {
910
let value: (Node) -> Node.Loader.Value
11+
12+
func scoped(key: ScopeKey) -> any AtomScopedOverrideProtocol {
13+
AtomScopedOverride<Node>(scopeKey: key, value: value)
14+
}
15+
}
16+
17+
// As a workaround to the problem of not getting ScopeKey synchronously
18+
// when the AtomRoot or AtomScope's override modifier is called, those modifiers
19+
// temporarily register AtomOverride and convert them to AtomScopedOverride when
20+
// their View body is evaluated. This is not ideal from a performance standpoint,
21+
// so it will be improved as soon as an alternative way to grant per-scope keys
22+
// independent of the SwiftUI lifecycle is came up.
23+
internal protocol AtomScopedOverrideProtocol {
24+
var scopeKey: ScopeKey { get }
25+
}
26+
27+
internal struct AtomScopedOverride<Node: Atom>: AtomScopedOverrideProtocol {
28+
let scopeKey: ScopeKey
29+
let value: (Node) -> Node.Loader.Value
1030
}

Sources/Atoms/Core/ScopeKey.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
internal struct ScopeKey: Hashable {
2+
final class Token {}
3+
4+
private let identifier: ObjectIdentifier
5+
6+
var id: String {
7+
String(hashValue, radix: 36, uppercase: false)
8+
}
9+
10+
init(token: Token) {
11+
identifier = ObjectIdentifier(token)
12+
}
13+
}

0 commit comments

Comments
 (0)