Skip to content

Commit 8b583f7

Browse files
authored
Fix issue where scoped overrides/observers are not reflected (#167)
* Add test cases * Propagate the transaction scope to transitively reflect the overrides * Improve test cases * Refactoring * Refactoring * Add test cases * Fix for Swift 5 compiler * Fix for Swift 6 compiler Swift 5 language mode * Add test case * Refactoring * Fix typo in comment: "dependants" to "dependents"
1 parent 8c75a97 commit 8b583f7

35 files changed

+937
-602
lines changed

Sources/Atoms/AtomRoot.swift

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public struct AtomRoot<Content: View>: View {
9999
)
100100

101101
case .unmanaged(let store):
102-
Unmanaged(
102+
Scope(
103103
store: store,
104104
overrides: overrides,
105105
observers: observers,
@@ -129,7 +129,7 @@ public struct AtomRoot<Content: View>: View {
129129
///
130130
/// - Returns: The self instance.
131131
public func override<Node: Atom>(_ atom: Node, with value: @escaping @MainActor @Sendable (Node) -> Node.Produced) -> Self {
132-
mutating(self) { $0.overrides[OverrideKey(atom)] = Override(isScoped: false, getValue: value) }
132+
mutating(self) { $0.overrides[OverrideKey(atom)] = Override(getValue: value) }
133133
}
134134

135135
/// Overrides the atoms with the given value.
@@ -145,7 +145,7 @@ public struct AtomRoot<Content: View>: View {
145145
///
146146
/// - Returns: The self instance.
147147
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping @MainActor @Sendable (Node) -> Node.Produced) -> Self {
148-
mutating(self) { $0.overrides[OverrideKey(atomType)] = Override(isScoped: false, getValue: value) }
148+
mutating(self) { $0.overrides[OverrideKey(atomType)] = Override(getValue: value) }
149149
}
150150
}
151151

@@ -163,46 +163,41 @@ private extension AtomRoot {
163163
@State
164164
private var store = AtomStore()
165165
@State
166-
private var token = ScopeKey.Token()
166+
private var state = ScopeState()
167167

168168
var body: some View {
169-
content.environment(
170-
\.store,
171-
StoreContext(
172-
store: store,
173-
scopeKey: ScopeKey(token: token),
174-
inheritedScopeKeys: [:],
175-
observers: observers,
176-
scopedObservers: [],
177-
overrides: overrides,
178-
scopedOverrides: [:]
179-
)
169+
Scope(
170+
store: store,
171+
overrides: overrides,
172+
observers: observers,
173+
content: content
180174
)
181175
}
182176
}
183177

184-
struct Unmanaged: View {
178+
struct Scope: View {
185179
let store: AtomStore
186180
let overrides: [OverrideKey: any OverrideProtocol]
187181
let observers: [Observer]
188182
let content: Content
189183

190184
@State
191-
private var token = ScopeKey.Token()
185+
private var state = ScopeState()
192186

193187
var body: some View {
194-
content.environment(
195-
\.store,
196-
StoreContext(
197-
store: store,
198-
scopeKey: ScopeKey(token: token),
199-
inheritedScopeKeys: [:],
200-
observers: observers,
201-
scopedObservers: [],
202-
overrides: overrides,
203-
scopedOverrides: [:]
204-
)
188+
let scopeKey = state.token.key
189+
let store = StoreContext.registerRoot(
190+
in: store,
191+
scopeKey: scopeKey,
192+
overrides: overrides,
193+
observers: observers
205194
)
195+
196+
state.unregister = {
197+
store.unregister(scopeKey: scopeKey)
198+
}
199+
200+
return content.environment(\.store, store)
206201
}
207202
}
208203
}

Sources/Atoms/AtomScope.swift

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ import SwiftUI
4949
///
5050
public struct AtomScope<Content: View>: View {
5151
private let inheritance: Inheritance
52-
private var overrides: [OverrideKey: any OverrideProtocol]
53-
private var observers: [Observer]
52+
private var overrides = [OverrideKey: any OverrideProtocol]()
53+
private var observers = [Observer]()
5454
private let content: Content
5555

5656
/// Creates a new scope with the specified content.
@@ -61,8 +61,6 @@ public struct AtomScope<Content: View>: View {
6161
public init<ID: Hashable>(id: ID = DefaultScopeID(), @ViewBuilder content: () -> Content) {
6262
let id = ScopeID(id)
6363
self.inheritance = .environment(id: id)
64-
self.overrides = [:]
65-
self.observers = []
6664
self.content = content()
6765
}
6866

@@ -78,8 +76,6 @@ public struct AtomScope<Content: View>: View {
7876
) {
7977
let store = context._store
8078
self.inheritance = .context(store: store)
81-
self.overrides = store.scopedOverrides
82-
self.observers = store.scopedObservers
8379
self.content = content()
8480
}
8581

@@ -97,8 +93,6 @@ public struct AtomScope<Content: View>: View {
9793
case .context(let store):
9894
WithContext(
9995
store: store,
100-
overrides: overrides,
101-
observers: observers,
10296
content: content
10397
)
10498
}
@@ -110,6 +104,8 @@ public struct AtomScope<Content: View>: View {
110104
/// Note that unlike ``AtomRoot/observe(_:)``, this observes only the state changes caused by atoms
111105
/// used in this scope.
112106
///
107+
/// - Note: It ignores the observers if this scope inherits the parent scope.
108+
///
113109
/// - Parameter onUpdate: A closure to handle a snapshot of recent updates.
114110
///
115111
/// - Returns: The self instance.
@@ -124,13 +120,15 @@ public struct AtomScope<Content: View>: View {
124120
///
125121
/// This only overrides atoms used in this scope and never be inherited to a nested scopes.
126122
///
123+
/// - Note: It ignores the overrides if this scope inherits the parent scope.
124+
///
127125
/// - Parameters:
128126
/// - atom: An atom to be overridden.
129127
/// - value: A value to be used instead of the atom's value.
130128
///
131129
/// - Returns: The self instance.
132130
public func scopedOverride<Node: Atom>(_ atom: Node, with value: @escaping @MainActor @Sendable (Node) -> Node.Produced) -> Self {
133-
mutating(self) { $0.overrides[OverrideKey(atom)] = Override(isScoped: true, getValue: value) }
131+
mutating(self) { $0.overrides[OverrideKey(atom)] = Override(getValue: value) }
134132
}
135133

136134
/// Override the atoms used in this scope with the given value.
@@ -142,13 +140,15 @@ public struct AtomScope<Content: View>: View {
142140
///
143141
/// This only overrides atoms used in this scope and never be inherited to a nested scopes.
144142
///
143+
/// - Note: It ignores the overrides if this scope inherits the parent scope.
144+
///
145145
/// - Parameters:
146146
/// - atomType: An atom type to be overridden.
147147
/// - value: A value to be used instead of the atom's value.
148148
///
149149
/// - Returns: The self instance.
150150
public func scopedOverride<Node: Atom>(_ atomType: Node.Type, with value: @escaping @MainActor @Sendable (Node) -> Node.Produced) -> Self {
151-
mutating(self) { $0.overrides[OverrideKey(atomType)] = Override(isScoped: true, getValue: value) }
151+
mutating(self) { $0.overrides[OverrideKey(atomType)] = Override(getValue: value) }
152152
}
153153
}
154154

@@ -165,37 +165,33 @@ private extension AtomScope {
165165
let content: Content
166166

167167
@State
168-
private var token = ScopeKey.Token()
168+
private var state = ScopeState()
169169
@Environment(\.store)
170170
private var environmentStore
171171

172172
var body: some View {
173-
content.environment(
174-
\.store,
175-
environmentStore?.scoped(
176-
scopeKey: ScopeKey(token: token),
177-
scopeID: id,
178-
observers: observers,
179-
overrides: overrides
180-
)
173+
let scopeKey = state.token.key
174+
let store = environmentStore?.registerScope(
175+
scopeID: id,
176+
scopeKey: scopeKey,
177+
overrides: overrides,
178+
observers: observers
181179
)
180+
181+
state.unregister = {
182+
store?.unregister(scopeKey: scopeKey)
183+
}
184+
185+
return content.environment(\.store, store)
182186
}
183187
}
184188

185189
struct WithContext: View {
186190
let store: StoreContext
187-
let overrides: [OverrideKey: any OverrideProtocol]
188-
let observers: [Observer]
189191
let content: Content
190192

191193
var body: some View {
192-
content.environment(
193-
\.store,
194-
store.inherited(
195-
scopedObservers: observers,
196-
scopedOverrides: overrides
197-
)
198-
)
194+
content.environment(\.store, store)
199195
}
200196
}
201197
}

Sources/Atoms/Context/AtomTestContext.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ public struct AtomTestContext: AtomWatchableContext {
372372
/// - value: A value to be used instead of the atom's value.
373373
@inlinable
374374
public func override<Node: Atom>(_ atom: Node, with value: @escaping @MainActor @Sendable (Node) -> Node.Produced) {
375-
_state.overrides[OverrideKey(atom)] = Override(isScoped: false, getValue: value)
375+
_state.overrides[OverrideKey(atom)] = Override(getValue: value)
376376
}
377377

378378
/// Overrides the atom value with the given value.
@@ -387,7 +387,7 @@ public struct AtomTestContext: AtomWatchableContext {
387387
/// - value: A value to be used instead of the atom's value.
388388
@inlinable
389389
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping @MainActor @Sendable (Node) -> Node.Produced) {
390-
_state.overrides[OverrideKey(atomType)] = Override(isScoped: false, getValue: value)
390+
_state.overrides[OverrideKey(atomType)] = Override(getValue: value)
391391
}
392392
}
393393

@@ -437,14 +437,11 @@ internal extension AtomTestContext {
437437

438438
@usableFromInline
439439
var _store: StoreContext {
440-
StoreContext(
441-
store: _state.store,
442-
scopeKey: ScopeKey(token: _state.token),
443-
inheritedScopeKeys: [:],
444-
observers: [],
445-
scopedObservers: [],
440+
.registerRoot(
441+
in: _state.store,
442+
scopeKey: _state.token.key,
446443
overrides: _state.overrides,
447-
scopedOverrides: [:]
444+
observers: []
448445
)
449446
}
450447

Sources/Atoms/Core/AtomCache.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
internal protocol AtomCacheProtocol {
22
associatedtype Node: Atom
33

4-
var atom: Node { get set }
5-
var value: Node.Produced { get set }
4+
var atom: Node { get }
5+
var value: Node.Produced { get }
6+
var initScopeKey: ScopeKey? { get }
7+
8+
func updated(value: Node.Produced) -> Self
69
}
710

811
internal struct AtomCache<Node: Atom>: AtomCacheProtocol, CustomStringConvertible {
9-
var atom: Node
10-
var value: Node.Produced
12+
let atom: Node
13+
let value: Node.Produced
14+
let initScopeKey: ScopeKey?
1115

1216
var description: String {
1317
"\(value)"
1418
}
19+
20+
func updated(value: Node.Produced) -> Self {
21+
AtomCache(atom: atom, value: value, initScopeKey: initScopeKey)
22+
}
1523
}

Sources/Atoms/Core/Override.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,16 @@
22
internal protocol OverrideProtocol: Sendable {
33
associatedtype Node: Atom
44

5-
var isScoped: Bool { get }
65
var getValue: @MainActor @Sendable (Node) -> Node.Produced { get }
76
}
87

98
@usableFromInline
109
internal struct Override<Node: Atom>: OverrideProtocol {
11-
@usableFromInline
12-
let isScoped: Bool
1310
@usableFromInline
1411
let getValue: @MainActor @Sendable (Node) -> Node.Produced
1512

1613
@usableFromInline
17-
init(isScoped: Bool, getValue: @escaping @MainActor @Sendable (Node) -> Node.Produced) {
18-
self.isScoped = isScoped
14+
init(getValue: @escaping @MainActor @Sendable (Node) -> Node.Produced) {
1915
self.getValue = getValue
2016
}
2117
}

Sources/Atoms/Core/Scope.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
internal struct Scope {
2+
let overrides: [OverrideKey: any OverrideProtocol]
3+
let observers: [Observer]
4+
let ancestorScopeKeys: [ScopeID: ScopeKey]
5+
}

Sources/Atoms/Core/ScopeKey.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@usableFromInline
22
internal struct ScopeKey: Hashable, Sendable, CustomStringConvertible {
3-
final class Token {}
3+
final class Token {
4+
private(set) lazy var key = ScopeKey(token: self)
5+
}
46

57
private let identifier: ObjectIdentifier
68

@@ -9,7 +11,7 @@ internal struct ScopeKey: Hashable, Sendable, CustomStringConvertible {
911
String(hashValue, radix: 36, uppercase: false)
1012
}
1113

12-
init(token: Token) {
14+
private init(token: Token) {
1315
identifier = ObjectIdentifier(token)
1416
}
1517
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
@MainActor
2+
internal final class ScopeState {
3+
let token = ScopeKey.Token()
4+
5+
#if !hasFeature(IsolatedDefaultValues)
6+
nonisolated init() {}
7+
#endif
8+
9+
#if compiler(>=6)
10+
nonisolated(unsafe) var unregister: (@MainActor () -> Void)?
11+
12+
// TODO: Use isolated synchronous deinit once it's available.
13+
// 0371-isolated-synchronous-deinit
14+
deinit {
15+
MainActor.performIsolated { [unregister] in
16+
unregister?()
17+
}
18+
}
19+
#else
20+
private var _unregister = UnsafeUncheckedSendable<(@MainActor () -> Void)?>(nil)
21+
22+
var unregister: (@MainActor () -> Void)? {
23+
_read { yield _unregister.value }
24+
_modify { yield &_unregister.value }
25+
}
26+
27+
deinit {
28+
MainActor.performIsolated { [_unregister] in
29+
_unregister.value?()
30+
}
31+
}
32+
#endif
33+
}

0 commit comments

Comments
 (0)