Skip to content

Commit

Permalink
Implement scoped atom feature
Browse files Browse the repository at this point in the history
  • Loading branch information
ra1028 committed Apr 16, 2024
1 parent c686675 commit ae553ae
Show file tree
Hide file tree
Showing 12 changed files with 90 additions and 32 deletions.
14 changes: 5 additions & 9 deletions Sources/Atoms/AtomRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public struct AtomRoot<Content: View>: View {
///
/// - Returns: The self instance.
public func observe(_ onUpdate: @escaping @MainActor (Snapshot) -> Void) -> Self {
mutating { $0.observers.append(Observer(onUpdate: onUpdate)) }
mutating(self) { $0.observers.append(Observer(onUpdate: onUpdate)) }
}

/// Overrides the atom value with the given value.
Expand All @@ -130,7 +130,7 @@ public struct AtomRoot<Content: View>: View {
///
/// - Returns: The self instance.
public func override<Node: Atom>(_ atom: Node, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
mutating { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
mutating(self) { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
}

/// Overrides the atom value with the given value.
Expand All @@ -146,7 +146,7 @@ public struct AtomRoot<Content: View>: View {
///
/// - Returns: The self instance.
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
mutating { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
mutating(self) { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
}
}

Expand Down Expand Up @@ -176,6 +176,7 @@ private extension AtomRoot {
StoreContext(
state.store,
scopeKey: ScopeKey(token: state.token),
inheritedScopeKeys: [:],
observers: observers,
overrides: overrides
)
Expand Down Expand Up @@ -203,16 +204,11 @@ private extension AtomRoot {
StoreContext(
store,
scopeKey: ScopeKey(token: state.token),
inheritedScopeKeys: [:],
observers: observers,
overrides: overrides
)
)
}
}

func `mutating`(_ mutation: (inout Self) -> Void) -> Self {
var view = self
mutation(&view)
return view
}
}
28 changes: 14 additions & 14 deletions Sources/Atoms/AtomScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ public struct AtomScope<Content: View>: View {

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

Expand All @@ -66,8 +69,9 @@ public struct AtomScope<Content: View>: View {
/// The content and behavior of the view.
public var body: some View {
switch inheritance {
case .environment:
case .environment(let id):
InheritedEnvironment(
id: id,
content: content,
overrides: overrides,
observers: observers
Expand All @@ -94,7 +98,7 @@ public struct AtomScope<Content: View>: View {
///
/// - Returns: The self instance.
public func observe(_ onUpdate: @escaping @MainActor (Snapshot) -> Void) -> Self {
mutating { $0.observers.append(Observer(onUpdate: onUpdate)) }
mutating(self) { $0.observers.append(Observer(onUpdate: onUpdate)) }
}

/// Override the atom value used in this scope with the given value.
Expand All @@ -110,7 +114,7 @@ public struct AtomScope<Content: View>: View {
///
/// - Returns: The self instance.
public func override<Node: Atom>(_ atom: Node, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
mutating { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
mutating(self) { $0.overrides[OverrideKey(atom)] = AtomOverride(value: value) }
}

/// Override the atom value used in this scope with the given value.
Expand All @@ -128,13 +132,13 @@ public struct AtomScope<Content: View>: View {
///
/// - Returns: The self instance.
public func override<Node: Atom>(_ atomType: Node.Type, with value: @escaping (Node) -> Node.Loader.Value) -> Self {
mutating { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
mutating(self) { $0.overrides[OverrideKey(atomType)] = AtomOverride(value: value) }
}
}

private extension AtomScope {
enum Inheritance {
case environment
case environment(id: ScopeID)
case context(AtomViewContext)
}

Expand All @@ -144,6 +148,7 @@ private extension AtomScope {
let token = ScopeKey.Token()
}

let id: ScopeID
let content: Content
let overrides: [OverrideKey: any AtomOverrideProtocol]
let observers: [Observer]
Expand All @@ -158,6 +163,7 @@ private extension AtomScope {
\.store,
environmentStore.scoped(
scopeKey: ScopeKey(token: state.token),
scopeID: id,
observers: observers,
overrides: overrides
)
Expand All @@ -181,10 +187,4 @@ private extension AtomScope {
)
}
}

func `mutating`(_ mutation: (inout Self) -> Void) -> Self {
var view = self
mutation(&view)
return view
}
}
2 changes: 2 additions & 0 deletions Sources/Atoms/Atoms.docc/Atoms.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Building state by compositing atoms automatically optimizes rendering based on i

### Attributes

- ``Scoped``
- ``KeepAlive``
- ``Refreshable``
- ``Resettable``
Expand Down Expand Up @@ -85,3 +86,4 @@ Building state by compositing atoms automatically optimizes rendering based on i
- ``ObservableObjectAtomLoader``
- ``ModifiedAtomLoader``
- ``AtomLoaderContext``
- ``DefaultScopeID``
2 changes: 1 addition & 1 deletion Sources/Atoms/Attribute/KeepAlive.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// An attribute protocol to allow the value of an atom to continue being retained
/// even after they are no longer watched.
///
/// Note that overridden atoms are not retained even with this attribute.
/// Note that overridden or scoped atoms are not retained even with this attribute.
///
/// ## Example
///
Expand Down
15 changes: 15 additions & 0 deletions Sources/Atoms/Attribute/Scoped.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
public protocol Scoped where Self: Atom {
associatedtype ScopeID: Hashable = DefaultScopeID

var scopeID: ScopeID { get }
}

public extension Scoped where ScopeID == DefaultScopeID {
var scopeID: ScopeID {
DefaultScopeID()
}
}

public struct DefaultScopeID: Hashable {
public init() {}
}
1 change: 1 addition & 0 deletions Sources/Atoms/Context/AtomTestContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ internal extension AtomTestContext {
StoreContext(
_state.store,
scopeKey: ScopeKey(token: _state.token),
inheritedScopeKeys: [:],
observers: [],
overrides: _state.overrides
)
Expand Down
1 change: 1 addition & 0 deletions Sources/Atoms/Core/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ private struct StoreEnvironmentKey: EnvironmentKey {
StoreContext(
nil,
scopeKey: ScopeKey(token: ScopeKey.Token()),
inheritedScopeKeys: [:],
observers: [],
overrides: [:],
enablesAssertion: true
Expand Down
7 changes: 7 additions & 0 deletions Sources/Atoms/Core/ScopeID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
internal struct ScopeID: Hashable {
private let id: AnyHashable

init(_ id: any Hashable) {
self.id = AnyHashable(id)
}
}
35 changes: 27 additions & 8 deletions Sources/Atoms/Core/StoreContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ import Foundation
internal struct StoreContext {
private weak var weakStore: AtomStore?
private let scopeKey: ScopeKey
private let inheritedScopeKeys: [ScopeID: ScopeKey]
private let observers: [Observer]
private let overrides: [OverrideKey: any AtomOverrideProtocol]
private let enablesAssertion: Bool

nonisolated init(
_ store: AtomStore?,
scopeKey: ScopeKey,
inheritedScopeKeys: [ScopeID: ScopeKey],
observers: [Observer],
overrides: [OverrideKey: any AtomOverrideProtocol],
enablesAssertion: Bool = false
) {
self.weakStore = store
self.scopeKey = scopeKey
self.inheritedScopeKeys = inheritedScopeKeys
self.observers = observers
self.overrides = overrides
self.enablesAssertion = enablesAssertion
Expand All @@ -30,6 +33,7 @@ internal struct StoreContext {
StoreContext(
weakStore,
scopeKey: scopeKey,
inheritedScopeKeys: inheritedScopeKeys,
observers: self.observers + observers,
overrides: self.overrides.merging(overrides) { $1 },
enablesAssertion: enablesAssertion
Expand All @@ -38,12 +42,14 @@ internal struct StoreContext {

func scoped(
scopeKey: ScopeKey,
scopeID: ScopeID,
observers: [Observer],
overrides: [OverrideKey: any AtomOverrideProtocol]
) -> StoreContext {
StoreContext(
weakStore,
scopeKey: scopeKey,
inheritedScopeKeys: mutating(inheritedScopeKeys) { $0[scopeID] = scopeKey },
observers: self.observers + observers,
overrides: overrides,
enablesAssertion: enablesAssertion
Expand All @@ -53,7 +59,7 @@ internal struct StoreContext {
@usableFromInline
func read<Node: Atom>(_ atom: Node) -> Node.Loader.Value {
let override = lookupOverride(of: atom)
let scopeKey = override != nil ? scopeKey : nil
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
let key = AtomKey(atom, scopeKey: scopeKey)

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

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

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

let store = getStore()
let override = lookupOverride(of: atom)
let scopeKey = override != nil ? scopeKey : nil
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
let key = AtomKey(atom, scopeKey: scopeKey)
let newCache = lookupCache(of: atom, for: key) ?? makeNewCache(of: atom, for: key, override: override)

Expand All @@ -123,7 +129,7 @@ internal struct StoreContext {
) -> Node.Loader.Value {
let store = getStore()
let override = lookupOverride(of: atom)
let scopeKey = override != nil ? scopeKey : nil
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
let key = AtomKey(atom, scopeKey: scopeKey)
let newCache = lookupCache(of: atom, for: key) ?? makeNewCache(of: atom, for: key, override: override)
let subscription = Subscription(
Expand All @@ -149,7 +155,7 @@ internal struct StoreContext {
@_disfavoredOverload
func refresh<Node: Atom>(_ atom: Node) async -> Node.Loader.Value where Node.Loader: RefreshableAtomLoader {
let override = lookupOverride(of: atom)
let scopeKey = override != nil ? scopeKey : nil
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
let key = AtomKey(atom, scopeKey: scopeKey)
let context = prepareForTransaction(of: atom, for: key)
let value: Node.Loader.Value
Expand Down Expand Up @@ -179,7 +185,7 @@ internal struct StoreContext {
@usableFromInline
func refresh<Node: Refreshable>(_ atom: Node) async -> Node.Loader.Value {
let override = lookupOverride(of: atom)
let scopeKey = override != nil ? scopeKey : nil
let scopeKey = lookupScopeKey(of: atom, isOverridden: override != nil)
let key = AtomKey(atom, scopeKey: scopeKey)
let state = getState(of: atom, for: key)
let value: Node.Loader.Value
Expand Down Expand Up @@ -445,7 +451,7 @@ private extension StoreContext {
let store = getStore()

// The condition under which an atom may be released are as follows:
// 1. It's not marked as `KeepAlive` or is overridden.
// 1. It's not marked as `KeepAlive`, is marked as `Scoped`, or is scoped by override.
// 2. It has no downstream atoms.
// 3. It has no subscriptions from views.
lazy var shouldKeepAlive = !key.isScoped && store.state.caches[key].map { $0.atom is any KeepAlive } ?? false
Expand Down Expand Up @@ -592,6 +598,19 @@ private extension StoreContext {
return override
}

func lookupScopeKey<Node: Atom>(of atom: Node, isOverridden: Bool) -> ScopeKey? {
if isOverridden {
return scopeKey
}
else if let atom = atom as? any Scoped {
let scopeID = ScopeID(atom.scopeID)
return inheritedScopeKeys[scopeID]
}
else {
return nil
}
}

func notifyUpdateToObservers() {
guard !observers.isEmpty else {
return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
func `mutating`<T>(_ value: T, _ mutation: (inout T) -> Void) -> T {
var value = value
mutation(&value)
return value
}

internal extension Task where Success == Never, Failure == Never {
@inlinable
static func sleep(seconds duration: Double) async throws {
Expand Down
Loading

0 comments on commit ae553ae

Please sign in to comment.