Skip to content

Commit 002f10d

Browse files
authored
Allow keeping Atoms alive within a scope until it's deallocation (#188)
* Keep an atom alive until its scope is unregistered if it conforms both KeepAlive and Scoped * Refactoring * Add test case * Update documentation * Refactoring
1 parent b1e2ea2 commit 002f10d

17 files changed

+234
-107
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,7 @@ VStack {
849849
#### [KeepAlive](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/keepalive)
850850

851851
`KeepAlive` allows the atom to preserve its data even if it's no longer watched from anywhere.
852+
When used with the [Scoped](#scoped) attribute, the atom's data is preserved until the scope ([AtomScope](#atomscope)) it belongs to is dismantled from the view tree. This is useful when you want to prevent the atom from being released prematurely within a specific scope.
852853

853854
<details><summary><code>📖 Example</code></summary>
854855

Sources/Atoms/AtomScope.swift

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ public struct AtomScope<Content: View>: View {
150150
}
151151

152152
private extension AtomScope {
153+
@MainActor
154+
final class State: ObservableObject {
155+
let scopeState = ScopeState()
156+
}
157+
153158
enum Inheritance {
154159
case environment(scopeID: ScopeID)
155160
case context(AtomViewContext)
@@ -161,21 +166,23 @@ private extension AtomScope {
161166
let overrideContainer: OverrideContainer
162167
let content: Content
163168

164-
@State
165-
private var scopeToken = ScopeKey.Token()
166169
@Environment(\.store)
167-
private var environmentStore
170+
private var store
171+
172+
@StateObject
173+
private var state = State()
168174

169175
var body: some View {
170-
content.environment(
171-
\.store,
172-
environmentStore?.scoped(
173-
scopeID: scopeID,
174-
scopeKey: scopeToken.key,
175-
observers: observers,
176-
overrideContainer: overrideContainer
177-
)
176+
let scopedStore = store?.scoped(
177+
scopeID: scopeID,
178+
scopeKey: state.scopeState.token.key,
179+
observers: observers,
180+
overrideContainer: overrideContainer
178181
)
182+
183+
scopedStore?.registerScope(state: state.scopeState)
184+
185+
return content.environment(\.store, scopedStore)
179186
}
180187
}
181188
}

Sources/Atoms/Attribute/KeepAlive.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
/// An attribute protocol to allow the value of an atom to continue being retained
22
/// even after they are no longer watched.
33
///
4-
/// Note that overridden or scoped atoms are not retained even with this attribute.
4+
/// ## Note
5+
///
6+
/// Atoms that conform to this attribute and are either scoped using the ``Scoped`` attribute
7+
/// or overridden via ``AtomScope/scopedOverride(_:with:)-5jen3`` are retained until their scope
8+
/// is dismantled from the view tree, after which they are released.
59
///
610
/// ## Example
711
///

Sources/Atoms/Core/AtomCache.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@ internal protocol AtomCacheProtocol {
33

44
var atom: Node { get }
55
var value: Node.Produced { get }
6-
var initializedScope: Scope? { get }
6+
var scopeValues: ScopeValues? { get }
7+
var shouldKeepAlive: Bool { get }
78

89
func updated(value: Node.Produced) -> Self
910
}
1011

1112
internal struct AtomCache<Node: Atom>: AtomCacheProtocol, CustomStringConvertible {
1213
let atom: Node
1314
let value: Node.Produced
14-
let initializedScope: Scope?
15+
let scopeValues: ScopeValues?
1516

1617
var description: String {
1718
"\(value)"
1819
}
1920

21+
var shouldKeepAlive: Bool {
22+
atom is any KeepAlive
23+
}
24+
2025
func updated(value: Node.Produced) -> Self {
21-
AtomCache(atom: atom, value: value, initializedScope: initializedScope)
26+
AtomCache(atom: atom, value: value, scopeValues: scopeValues)
2227
}
2328
}

Sources/Atoms/Core/AtomKey.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
internal struct AtomKey: Hashable, Sendable, CustomStringConvertible {
22
private let key: UnsafeUncheckedSendable<AnyHashable>
33
private let type: ObjectIdentifier
4-
private let scopeKey: ScopeKey?
54
private let anyAtomType: Any.Type
65

6+
let scopeKey: ScopeKey?
7+
78
var description: String {
89
let atomLabel = String(describing: anyAtomType)
910

@@ -15,10 +16,6 @@ internal struct AtomKey: Hashable, Sendable, CustomStringConvertible {
1516
}
1617
}
1718

18-
var isScoped: Bool {
19-
scopeKey != nil
20-
}
21-
2219
init<Node: Atom>(_ atom: Node, scopeKey: ScopeKey?) {
2320
self.key = UnsafeUncheckedSendable(atom.key)
2421
self.type = ObjectIdentifier(Node.self)

Sources/Atoms/Core/Graph.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
internal struct Graph: Equatable {
22
var dependencies = [AtomKey: Set<AtomKey>]()
33
var children = [AtomKey: Set<AtomKey>]()
4-
var subscribed = [SubscriberKey: Set<AtomKey>]()
54
}

Sources/Atoms/Core/Scope.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1+
@MainActor
12
internal struct Scope {
2-
let key: ScopeKey
3-
let observers: [Observer]
4-
let overrideContainer: OverrideContainer
5-
let ancestorScopeKeys: [ScopeID: ScopeKey]
3+
var atoms = Set<AtomKey>()
64
}

Sources/Atoms/Core/ScopeKey.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@usableFromInline
22
internal struct ScopeKey: Hashable, Sendable, CustomStringConvertible {
3+
@MainActor
34
final class Token {
45
private(set) lazy var key = ScopeKey(token: self)
56
}

Sources/Atoms/Core/ScopeState.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@usableFromInline
2+
@MainActor
3+
internal final class ScopeState {
4+
let token = ScopeKey.Token()
5+
6+
#if !hasFeature(IsolatedDefaultValues)
7+
nonisolated init() {}
8+
#endif
9+
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+
}

Sources/Atoms/Core/ScopeValues.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
internal struct ScopeValues {
2+
let key: ScopeKey
3+
let observers: [Observer]
4+
let overrideContainer: OverrideContainer
5+
let ancestorScopeKeys: [ScopeID: ScopeKey]
6+
}

0 commit comments

Comments
 (0)