Skip to content

Commit 0da7155

Browse files
authored
Scoped Override (#112)
* Implement scoped override feature * Add comment for future optimization
1 parent 2d4635c commit 0da7155

15 files changed

+268
-99
lines changed

README.md

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,33 +1307,52 @@ Note that other atoms that depend on scoped atoms will be in a shared state and
13071307

13081308
#### Override Atoms
13091309

1310-
Overriding an atom in [AtomRoot](#atomroot) or [AtomScope](#atomscope) overwrites its state when used in the descendant views, which is useful for dependency injection or swapping state in a particular view.
1310+
You can override atoms in [AtomRoot](#atomroot) or [AtomScope](#atomscope) to overwirete the atom states for dependency injection or faking state in particular view, which is useful especially for testing.
1311+
1312+
Overriding in `AtomRoot` return the given value instead of the actual atom value no matter where the overridden atom is used in its descendant views.
13111313

13121314
```swift
1315+
// Overrides the CounterAtom value to be `456` in anywhere in the ancestor.
1316+
AtomRoot {
1317+
RootView()
1318+
}
1319+
.override(CounterAtom()) { _ in
1320+
456
1321+
}
1322+
```
1323+
1324+
On the other hand, overriding with `AtomScope` behaves similar to overriding in `AtomRoot`, but the atoms used in other scopes nested in the descendants are not overridden.
1325+
1326+
```swift
1327+
// Overrides the CounterAtom value to be `456` only for this scope.
13131328
AtomScope {
13141329
CountDisplay()
1330+
1331+
// CounterAtom is not overridden in this scope.
1332+
AtomScope {
1333+
CountDisplay()
1334+
}
13151335
}
1316-
.override(CounterAtom()) { _ in
1317-
456 // Overrides the count to be `456` only for this scope.
1336+
.scopedOverride(CounterAtom()) { _ in
1337+
456
13181338
}
13191339
```
13201340

1321-
Note that when multiple `AtomScope`s are nested, it doesn't inherit the overrides of its ancestor scopes.
1322-
In this case, you can explicitly inherit overrides from the parent scope by passing a `@ViewContext` context that has gotten in the parent scope.
1341+
If you want to inherit the overridden atom from the parent scope, you can explicitly pass `@ViewContext` context that has gotten in the parent scope. Then, the new scope completely inherits the parent scope's context.
13231342

13241343
```swift
13251344
@ViewContext
13261345
var context
13271346

13281347
var body: some {
1329-
// Inherites the nearest ancester scope's overrides.
1348+
// Inherites the parent scope's overrides.
13301349
AtomScope(inheriting: context) {
13311350
CountDisplay()
13321351
}
13331352
}
13341353
```
13351354

1336-
Note also that overridden atoms will automatically be scoped to the `AtomScope`, but other atoms that depend on them will be in a shared state and must be given `Scoped` attribute (See also: [Scoped Atoms](#scoped-atoms)) in order to scope them as well.
1355+
Note that overridden atoms in `AtomScope` automatically be scoped, but other atoms that depend on them will be in a shared state and must be given `Scoped` attribute (See also: [Scoped Atoms](#scoped-atoms)) in order to avoid it from being shared across out of scope.
13371356

13381357
See [Testing](#testing) section for details on dependency injection on unit tests.
13391358

@@ -1501,7 +1520,8 @@ digraph {
15011520

15021521
#### Preview
15031522

1504-
Even in SwiftUI previews, the view must have an `AtomRoot` somewhere in the ancestor. However, since This library offers the new solution for dependency injection, you don't need to do painful DI each time you create previews anymore. You can to override the atoms that you really want to inject substitutions.
1523+
Even in SwiftUI previews, the view must have an `AtomRoot` somewhere in the ancestor.
1524+
To inject dependencies so that display a static preview, define the dependencies as atoms and override them.
15051525

15061526
```swift
15071527
struct NewsList_Preview: PreviewProvider {
@@ -1516,6 +1536,8 @@ struct NewsList_Preview: PreviewProvider {
15161536
}
15171537
```
15181538

1539+
See [Override Atoms](#override-atoms) section for more details of dependency injection.
1540+
15191541
---
15201542

15211543
### Advanced Usage

Sources/Atoms/AtomRoot.swift

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ import SwiftUI
1717
/// }
1818
/// ```
1919
///
20-
/// Optionally, this component allows you to override a value of arbitrary atoms, that's useful
20+
/// This view allows you to override a value of arbitrary atoms, which is useful
2121
/// for dependency injection in testing.
2222
///
2323
/// ```swift
2424
/// AtomRoot {
25-
/// MyView()
25+
/// RootView()
2626
/// }
27-
/// .override(RepositoryAtom()) {
28-
/// FakeRepository()
27+
/// .override(APIClientAtom()) {
28+
/// StubAPIClient()
2929
/// }
3030
/// ```
3131
///
@@ -118,34 +118,34 @@ public struct AtomRoot<Content: View>: View {
118118
mutating(self) { $0.observers.append(Observer(onUpdate: onUpdate)) }
119119
}
120120

121-
/// Overrides the atom value with the given value.
121+
/// Overrides the atoms with the given value.
122122
///
123-
/// When accessing the overridden atom, this context will create and return the given value
124-
/// instead of the atom value.
123+
/// It will create and return the given value instead of the actual atom value when accessing
124+
/// the overridden atom in any scopes.
125125
///
126126
/// - Parameters:
127127
/// - atom: An atom to be overridden.
128128
/// - value: A value to be used instead of the atom's value.
129129
///
130130
/// - Returns: The self instance.
131131
public func override<Node: Atom>(_ atom: Node, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
132-
mutating(self) { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
132+
mutating(self) { $0.overrides[OverrideKey(atom)] = AtomOverride(isScoped: false, value: value) }
133133
}
134134

135-
/// Overrides the atom value with the given value.
135+
/// Overrides the atoms with the given value.
136136
///
137-
/// Instead of overriding the particular instance of atom, this method overrides any atom that
138-
/// has the same metatype.
139-
/// When accessing the overridden atom, this context will create and return the given value
140-
/// instead of the atom value.
137+
/// It will create and return the given value instead of the actual atom value when accessing
138+
/// the overridden atom in any scopes.
139+
/// This method overrides any atoms that has the same metatype, instead of overriding
140+
/// the particular instance of atom.
141141
///
142142
/// - Parameters:
143143
/// - atomType: An atom type to be overridden.
144144
/// - value: A value to be used instead of the atom's value.
145145
///
146146
/// - Returns: The self instance.
147147
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
148-
mutating(self) { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
148+
mutating(self) { $0.overrides[OverrideKey(atomType)] = AtomOverride(isScoped: false, value: value) }
149149
}
150150
}
151151

@@ -178,7 +178,8 @@ private extension AtomRoot {
178178
inheritedScopeKeys: [:],
179179
observers: observers,
180180
scopedObservers: [],
181-
overrides: overrides
181+
overrides: overrides,
182+
scopedOverrides: [:]
182183
)
183184
)
184185
}
@@ -207,7 +208,8 @@ private extension AtomRoot {
207208
inheritedScopeKeys: [:],
208209
observers: observers,
209210
scopedObservers: [],
210-
overrides: overrides
211+
overrides: overrides,
212+
scopedOverrides: [:]
211213
)
212214
)
213215
}

Sources/Atoms/AtomScope.swift

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,19 @@ import SwiftUI
22

33
/// A view to override or monitor atoms in scope.
44
///
5-
/// This view allows you to monitor changes of atoms used in descendant views by``AtomScope/scopedObserve(_:)``.
5+
/// This view allows you to override a value of arbitrary atoms used in this scope, which is useful
6+
/// for dependency injection in testing.
7+
///
8+
/// ```swift
9+
/// AtomScope {
10+
/// MyView()
11+
/// }
12+
/// .scopedOverride(APIClientAtom()) {
13+
/// StubAPIClient()
14+
/// }
15+
/// ```
16+
///
17+
/// You can also observe updates with a snapshot that captures a specific set of values of atoms.
618
///
719
/// ```swift
820
/// AtomScope {
@@ -100,38 +112,38 @@ public struct AtomScope<Content: View>: View {
100112
mutating(self) { $0.observers.append(Observer(onUpdate: onUpdate)) }
101113
}
102114

103-
/// Override the atom value used in this scope with the given value.
115+
/// Override the atoms used in this scope with the given value.
104116
///
105-
/// When accessing the overridden atom, this context will create and return the given value
106-
/// instead of the atom value.
117+
/// It will create and return the given value instead of the actual atom value when accessing
118+
/// the overridden atom in this scope.
107119
///
108-
/// This only overrides atoms used in this scope and never be inherited to a nested scope.
120+
/// This only overrides atoms used in this scope and never be inherited to a nested scopes.
109121
///
110122
/// - Parameters:
111123
/// - atom: An atom to be overridden.
112124
/// - value: A value to be used instead of the atom's value.
113125
///
114126
/// - Returns: The self instance.
115-
public func override<Node: Atom>(_ atom: Node, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
116-
mutating(self) { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
127+
public func scopedOverride<Node: Atom>(_ atom: Node, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
128+
mutating(self) { $0.overrides[OverrideKey(atom)] = AtomOverride(isScoped: true, value: value) }
117129
}
118130

119-
/// Override the atom value used in this scope with the given value.
131+
/// Override the atoms used in this scope with the given value.
120132
///
121-
/// Instead of overriding the particular instance of atom, this method overrides any atom that
122-
/// has the same metatype.
123-
/// When accessing the overridden atom, this context will create and return the given value
124-
/// instead of the atom value.
133+
/// It will create and return the given value instead of the actual atom value when accessing
134+
/// the overridden atom in this scope.
135+
/// This method overrides any atoms that has the same metatype, instead of overriding
136+
/// the particular instance of atom.
125137
///
126-
/// This only overrides atoms used in this scope and never be inherited to a nested scope.
138+
/// This only overrides atoms used in this scope and never be inherited to a nested scopes.
127139
///
128140
/// - Parameters:
129141
/// - atomType: An atom type to be overridden.
130142
/// - value: A value to be used instead of the atom's value.
131143
///
132144
/// - Returns: The self instance.
133-
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
134-
mutating(self) { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
145+
public func scopedOverride<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
146+
mutating(self) { $0.overrides[OverrideKey(atomType)] = AtomOverride(isScoped: true, value: value) }
135147
}
136148
}
137149

@@ -181,7 +193,7 @@ private extension AtomScope {
181193
\.store,
182194
context._store.inherited(
183195
scopedObservers: observers,
184-
overrides: overrides
196+
scopedOverrides: overrides
185197
)
186198
)
187199
}

Sources/Atoms/Context/AtomTestContext.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ public struct AtomTestContext: AtomWatchableContext {
366366
/// - value: A value to be used instead of the atom's value.
367367
@inlinable
368368
public func override<Node: Atom>(_ atom: Node, with value: @escaping (Node) -> Node.Loader.Value) {
369-
_state.overrides[OverrideKey(atom)] = AtomOverride(value: value)
369+
_state.overrides[OverrideKey(atom)] = AtomOverride(isScoped: false, value: value)
370370
}
371371

372372
/// Overrides the atom value with the given value.
@@ -381,7 +381,7 @@ public struct AtomTestContext: AtomWatchableContext {
381381
/// - value: A value to be used instead of the atom's value.
382382
@inlinable
383383
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) {
384-
_state.overrides[OverrideKey(atomType)] = AtomOverride(value: value)
384+
_state.overrides[OverrideKey(atomType)] = AtomOverride(isScoped: false, value: value)
385385
}
386386
}
387387

@@ -444,7 +444,8 @@ internal extension AtomTestContext {
444444
inheritedScopeKeys: [:],
445445
observers: [],
446446
scopedObservers: [],
447-
overrides: _state.overrides
447+
overrides: _state.overrides,
448+
scopedOverrides: [:]
448449
)
449450
}
450451

Sources/Atoms/Core/AtomOverride.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
internal protocol AtomOverrideProtocol {
33
associatedtype Node: Atom
44

5+
var isScoped: Bool { get }
56
var value: (Node) -> Node.Loader.Value { get }
67
}
78

89
@usableFromInline
910
internal struct AtomOverride<Node: Atom>: AtomOverrideProtocol {
11+
@usableFromInline
12+
let isScoped: Bool
1013
@usableFromInline
1114
let value: (Node) -> Node.Loader.Value
1215

1316
@usableFromInline
14-
init(value: @escaping (Node) -> Node.Loader.Value) {
17+
init(isScoped: Bool, value: @escaping (Node) -> Node.Loader.Value) {
18+
self.isScoped = isScoped
1519
self.value = value
1620
}
1721
}

Sources/Atoms/Core/Environment.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ private struct StoreEnvironmentKey: EnvironmentKey {
1616
observers: [],
1717
scopedObservers: [],
1818
overrides: [:],
19+
scopedOverrides: [:],
1920
enablesAssertion: true
2021
)
2122
}

0 commit comments

Comments
 (0)