Skip to content

Commit 38aae90

Browse files
committed
Implement scoped atom feature
1 parent ef3660c commit 38aae90

File tree

10 files changed

+86
-30
lines changed

10 files changed

+86
-30
lines changed

Sources/Atoms/AtomRoot.swift

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public struct AtomRoot<Content: View>: View {
6363
StoreContext(
6464
state.store,
6565
scopeKey: ScopeKey(token: state.token),
66+
inheritedScopeKeys: [:],
6667
observers: observers,
6768
overrides: overrides
6869
)
@@ -77,7 +78,7 @@ public struct AtomRoot<Content: View>: View {
7778
///
7879
/// - Returns: The self instance.
7980
public func observe(_ onUpdate: @escaping @MainActor (Snapshot) -> Void) -> Self {
80-
mutating { $0.observers.append(Observer(onUpdate: onUpdate)) }
81+
mutating(self) { $0.observers.append(Observer(onUpdate: onUpdate)) }
8182
}
8283

8384
/// Overrides the atom value with the given value.
@@ -91,7 +92,7 @@ public struct AtomRoot<Content: View>: View {
9192
///
9293
/// - Returns: The self instance.
9394
public func override<Node: Atom>(_ atom: Node, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
94-
mutating { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
95+
mutating(self) { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
9596
}
9697

9798
/// Overrides the atom value with the given value.
@@ -107,7 +108,7 @@ public struct AtomRoot<Content: View>: View {
107108
///
108109
/// - Returns: The self instance.
109110
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
110-
mutating { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
111+
mutating(self) { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
111112
}
112113
}
113114

@@ -117,10 +118,4 @@ private extension AtomRoot {
117118
let store = AtomStore()
118119
let token = ScopeKey.Token()
119120
}
120-
121-
func `mutating`(_ mutation: (inout Self) -> Void) -> Self {
122-
var view = self
123-
mutation(&view)
124-
return view
125-
}
126121
}

Sources/Atoms/AtomScope.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,12 @@ public struct AtomScope<Content: View>: View {
5757

5858
/// Creates a new scope with the specified content.
5959
///
60-
/// - Parameter content: The view content that inheriting from the parent.
61-
public init(@ViewBuilder content: () -> Content) {
62-
self.inheritance = .environment
60+
/// - Parameters:
61+
/// - id: An identifier represents this scope used for matching scoped atoms.
62+
/// - content: The view content that inheriting from the parent.
63+
public init<ID: Hashable>(id: ID = DefaultScopeID(), @ViewBuilder content: () -> Content) {
64+
let id = ScopeID(id)
65+
self.inheritance = .environment(id: id)
6366
self.content = content()
6467
}
6568

@@ -110,8 +113,9 @@ public struct AtomScope<Content: View>: View {
110113
observers: observers
111114
)
112115

113-
case .environment:
116+
case .environment(let id):
114117
InheritedEnvironment(
118+
id: id,
115119
content: content,
116120
overrides: overrides,
117121
observers: observers
@@ -130,7 +134,7 @@ public struct AtomScope<Content: View>: View {
130134
///
131135
/// - Returns: The self instance.
132136
public func observe(_ onUpdate: @escaping @MainActor (Snapshot) -> Void) -> Self {
133-
mutating { $0.observers.append(Observer(onUpdate: onUpdate)) }
137+
mutating(self) { $0.observers.append(Observer(onUpdate: onUpdate)) }
134138
}
135139

136140
/// Override the atom value used in this scope with the given value.
@@ -146,7 +150,7 @@ public struct AtomScope<Content: View>: View {
146150
///
147151
/// - Returns: The self instance.
148152
public func override<Node: Atom>(_ atom: Node, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
149-
mutating { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
153+
mutating(self) { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
150154
}
151155

152156
/// Override the atom value used in this scope with the given value.
@@ -164,15 +168,15 @@ public struct AtomScope<Content: View>: View {
164168
///
165169
/// - Returns: The self instance.
166170
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
167-
mutating { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
171+
mutating(self) { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
168172
}
169173
}
170174

171175
private extension AtomScope {
172176
enum Inheritance {
173177
case context(AtomViewContext)
174178
case store(AtomStore)
175-
case environment
179+
case environment(id: ScopeID)
176180
}
177181

178182
struct InheritedContext: View {
@@ -207,6 +211,7 @@ private extension AtomScope {
207211
StoreContext(
208212
store,
209213
scopeKey: ScopeKey(token: state.token),
214+
inheritedScopeKeys: [:],
210215
observers: observers,
211216
overrides: overrides
212217
)
@@ -215,6 +220,7 @@ private extension AtomScope {
215220
}
216221

217222
struct InheritedEnvironment: View {
223+
let id: ScopeID
218224
let content: Content
219225
let overrides: [OverrideKey: any AtomOverrideProtocol]
220226
let observers: [Observer]
@@ -229,6 +235,7 @@ private extension AtomScope {
229235
\.store,
230236
environmentStore.scoped(
231237
scopeKey: ScopeKey(token: state.token),
238+
scopeID: id,
232239
observers: observers,
233240
overrides: overrides
234241
)
@@ -240,10 +247,4 @@ private extension AtomScope {
240247
final class ScopeState: ObservableObject {
241248
let token = ScopeKey.Token()
242249
}
243-
244-
func `mutating`(_ mutation: (inout Self) -> Void) -> Self {
245-
var view = self
246-
mutation(&view)
247-
return view
248-
}
249250
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
public protocol Scoped where Self: Atom {
2+
associatedtype ScopeID: Hashable = DefaultScopeID
3+
4+
var scopeID: ScopeID { get }
5+
}
6+
7+
public extension Scoped where ScopeID == DefaultScopeID {
8+
var scopeID: ScopeID {
9+
DefaultScopeID()
10+
}
11+
}
12+
13+
public struct DefaultScopeID: Hashable {
14+
public init() {}
15+
}

Sources/Atoms/Context/AtomTestContext.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ internal extension AtomTestContext {
442442
StoreContext(
443443
_state.store,
444444
scopeKey: ScopeKey(token: _state.token),
445+
inheritedScopeKeys: [:],
445446
observers: [],
446447
overrides: _state.overrides
447448
)

Sources/Atoms/Core/Environment.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ private struct StoreEnvironmentKey: EnvironmentKey {
1212
StoreContext(
1313
nil,
1414
scopeKey: ScopeKey(token: ScopeKey.Token()),
15+
inheritedScopeKeys: [:],
1516
observers: [],
1617
overrides: [:],
1718
enablesAssertion: true

Sources/Atoms/Core/ScopeID.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
internal struct ScopeID: Hashable {
2+
private let id: AnyHashable
3+
4+
init(_ id: any Hashable) {
5+
self.id = AnyHashable(id)
6+
}
7+
}

Sources/Atoms/Core/StoreContext.swift

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ import Foundation
55
internal struct StoreContext {
66
private weak var weakStore: AtomStore?
77
private let scopeKey: ScopeKey
8+
private let inheritedScopeKeys: [ScopeID: ScopeKey]
89
private let observers: [Observer]
910
private let overrides: [OverrideKey: any AtomOverrideProtocol]
1011
private let enablesAssertion: Bool
1112

1213
nonisolated init(
1314
_ store: AtomStore?,
1415
scopeKey: ScopeKey,
16+
inheritedScopeKeys: [ScopeID: ScopeKey],
1517
observers: [Observer],
1618
overrides: [OverrideKey: any AtomOverrideProtocol],
1719
enablesAssertion: Bool = false
1820
) {
1921
self.weakStore = store
2022
self.scopeKey = scopeKey
23+
self.inheritedScopeKeys = inheritedScopeKeys
2124
self.observers = observers
2225
self.overrides = overrides
2326
self.enablesAssertion = enablesAssertion
@@ -30,6 +33,7 @@ internal struct StoreContext {
3033
StoreContext(
3134
weakStore,
3235
scopeKey: scopeKey,
36+
inheritedScopeKeys: inheritedScopeKeys,
3337
observers: self.observers + observers,
3438
overrides: self.overrides.merging(overrides) { $1 },
3539
enablesAssertion: enablesAssertion
@@ -38,12 +42,14 @@ internal struct StoreContext {
3842

3943
func scoped(
4044
scopeKey: ScopeKey,
45+
scopeID: ScopeID,
4146
observers: [Observer],
4247
overrides: [OverrideKey: any AtomOverrideProtocol]
4348
) -> StoreContext {
4449
StoreContext(
4550
weakStore,
4651
scopeKey: scopeKey,
52+
inheritedScopeKeys: mutating(inheritedScopeKeys) { $0[scopeID] = scopeKey },
4753
observers: self.observers + observers,
4854
overrides: overrides,
4955
enablesAssertion: enablesAssertion
@@ -53,7 +59,7 @@ internal struct StoreContext {
5359
@usableFromInline
5460
func read<Node: Atom>(_ atom: Node) -> Node.Loader.Value {
5561
let override = lookupOverride(of: atom)
56-
let scopeKey = override != nil ? scopeKey : nil
62+
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
5763
let key = AtomKey(atom, scopeKey: scopeKey)
5864

5965
if let cache = lookupCache(of: atom, for: key) {
@@ -74,7 +80,7 @@ internal struct StoreContext {
7480
@usableFromInline
7581
func set<Node: StateAtom>(_ value: Node.Loader.Value, for atom: Node) {
7682
let override = lookupOverride(of: atom)
77-
let scopeKey = override != nil ? scopeKey : nil
83+
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
7884
let key = AtomKey(atom, scopeKey: scopeKey)
7985

8086
if let cache = lookupCache(of: atom, for: key) {
@@ -85,7 +91,7 @@ internal struct StoreContext {
8591
@usableFromInline
8692
func modify<Node: StateAtom>(_ atom: Node, body: (inout Node.Loader.Value) -> Void) {
8793
let override = lookupOverride(of: atom)
88-
let scopeKey = override != nil ? scopeKey : nil
94+
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
8995
let key = AtomKey(atom, scopeKey: scopeKey)
9096

9197
if let cache = lookupCache(of: atom, for: key) {
@@ -103,7 +109,7 @@ internal struct StoreContext {
103109

104110
let store = getStore()
105111
let override = lookupOverride(of: atom)
106-
let scopeKey = override != nil ? scopeKey : nil
112+
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
107113
let key = AtomKey(atom, scopeKey: scopeKey)
108114
let newCache = lookupCache(of: atom, for: key) ?? makeNewCache(of: atom, for: key, override: override)
109115

@@ -123,7 +129,7 @@ internal struct StoreContext {
123129
) -> Node.Loader.Value {
124130
let store = getStore()
125131
let override = lookupOverride(of: atom)
126-
let scopeKey = override != nil ? scopeKey : nil
132+
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
127133
let key = AtomKey(atom, scopeKey: scopeKey)
128134
let newCache = lookupCache(of: atom, for: key) ?? makeNewCache(of: atom, for: key, override: override)
129135
let subscription = Subscription(
@@ -149,7 +155,7 @@ internal struct StoreContext {
149155
@_disfavoredOverload
150156
func refresh<Node: Atom>(_ atom: Node) async -> Node.Loader.Value where Node.Loader: RefreshableAtomLoader {
151157
let override = lookupOverride(of: atom)
152-
let scopeKey = override != nil ? scopeKey : nil
158+
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
153159
let key = AtomKey(atom, scopeKey: scopeKey)
154160
let context = prepareForTransaction(of: atom, for: key)
155161
let value: Node.Loader.Value
@@ -179,7 +185,7 @@ internal struct StoreContext {
179185
@usableFromInline
180186
func refresh<Node: Refreshable>(_ atom: Node) async -> Node.Loader.Value {
181187
let override = lookupOverride(of: atom)
182-
let scopeKey = override != nil ? scopeKey : nil
188+
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
183189
let key = AtomKey(atom, scopeKey: scopeKey)
184190
let state = getState(of: atom, for: key)
185191
let value: Node.Loader.Value
@@ -592,6 +598,19 @@ private extension StoreContext {
592598
return override
593599
}
594600

601+
func lookupScopeKey<Node: Atom>(of atom: Node, isOverridden: Bool) -> ScopeKey? {
602+
if isOverridden {
603+
return scopeKey
604+
}
605+
else if let atom = atom as? any Scoped {
606+
let scopeID = ScopeID(atom.scopeID)
607+
return inheritedScopeKeys[scopeID]
608+
}
609+
else {
610+
return nil
611+
}
612+
}
613+
595614
func notifyUpdateToObservers() {
596615
guard !observers.isEmpty else {
597616
return

Sources/Atoms/Core/TaskExtensions.swift renamed to Sources/Atoms/Core/Utilities.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
func `mutating`<T>(_ value: T, _ mutation: (inout T) -> Void) -> T {
2+
var value = value
3+
mutation(&value)
4+
return value
5+
}
6+
17
internal extension Task where Success == Never, Failure == Never {
28
@inlinable
39
static func sleep(seconds duration: Double) async throws {

Tests/AtomsTests/Core/StoreContextTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ final class StoreContextTests: XCTestCase {
1414
let context = StoreContext(
1515
store,
1616
scopeKey: scopeKey,
17+
inheritedScopeKeys: [:],
1718
observers: [],
1819
overrides: [
1920
OverrideKey(atom): AtomOverride<TestAtom<Int>> { _ in
@@ -53,6 +54,7 @@ final class StoreContextTests: XCTestCase {
5354
)
5455
let scopedContext = context.scoped(
5556
scopeKey: ScopeKey(token: ScopeKey.Token()),
57+
scopeID: ScopeID(DefaultScopeID()),
5658
observers: [observer1],
5759
overrides: [
5860
OverrideKey(atom): AtomOverride<TestValueAtom<Int>> { _ in
@@ -343,6 +345,7 @@ final class StoreContextTests: XCTestCase {
343345
let overrideAtomKey = AtomKey(atom, scopeKey: scopeKey)
344346
let scopedContext = context.scoped(
345347
scopeKey: scopeKey,
348+
scopeID: ScopeID(DefaultScopeID()),
346349
observers: [],
347350
overrides: [
348351
OverrideKey(atom): AtomOverride<TestPublisherAtom<Just<Int>>> { _ in .success(1) }
@@ -405,6 +408,7 @@ final class StoreContextTests: XCTestCase {
405408
let overrideAtomKey = AtomKey(atom, scopeKey: scopeKey)
406409
let scopedContext = context.scoped(
407410
scopeKey: scopeKey,
411+
scopeID: ScopeID(DefaultScopeID()),
408412
observers: [],
409413
overrides: [
410414
OverrideKey(atom): AtomOverride<TestCustomRefreshableAtom<Just<Int>>> { _ in .success(2) }
@@ -509,6 +513,7 @@ final class StoreContextTests: XCTestCase {
509513
let overrideAtomKey = AtomKey(atom, scopeKey: scopeKey)
510514
let scopedContext = context.scoped(
511515
scopeKey: scopeKey,
516+
scopeID: ScopeID(DefaultScopeID()),
512517
observers: [],
513518
overrides: [
514519
OverrideKey(atom): AtomOverride<TestCustomResettableAtom<Int>> { _ in 2 }
@@ -645,11 +650,13 @@ final class StoreContextTests: XCTestCase {
645650
let context = StoreContext(store)
646651
let scoped1Context = context.scoped(
647652
scopeKey: scope1Key,
653+
scopeID: ScopeID(DefaultScopeID()),
648654
observers: [],
649655
overrides: scoped1Overrides
650656
)
651657
let scoped2Context = scoped1Context.scoped(
652658
scopeKey: scope2Key,
659+
scopeID: ScopeID(DefaultScopeID()),
653660
observers: [],
654661
overrides: scoped2Overrides
655662
)
@@ -906,6 +913,7 @@ final class StoreContextTests: XCTestCase {
906913
let key = AtomKey(atom, scopeKey: scopeKey)
907914
let context = context.scoped(
908915
scopeKey: scopeKey,
916+
scopeID: ScopeID(DefaultScopeID()),
909917
observers: [],
910918
overrides: [
911919
OverrideKey(atom): AtomOverride<KeepAliveAtom<Int>> { _ in 10 }
@@ -948,11 +956,13 @@ final class StoreContextTests: XCTestCase {
948956
)
949957
let scoped1Context = context.scoped(
950958
scopeKey: scope1Key,
959+
scopeID: ScopeID(DefaultScopeID()),
951960
observers: [],
952961
overrides: [:]
953962
)
954963
let scoped2Context = scoped1Context.scoped(
955964
scopeKey: scope2Key,
965+
scopeID: ScopeID(DefaultScopeID()),
956966
observers: [Observer { scopedSnapshots.append($0) }],
957967
overrides: scopedOverride
958968
)

0 commit comments

Comments
 (0)