From b956bd099ebe8794f60c776a21a3fb3d97a64905 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 11 Jun 2026 03:03:59 +0800 Subject: [PATCH] Add LazyState --- .../OpenSwiftUI/Data/State/LazyState.swift | 157 ++++++++++++++++++ .../Data/State/StoredLocation.swift | 62 +++---- 2 files changed, 189 insertions(+), 30 deletions(-) create mode 100644 Sources/OpenSwiftUI/Data/State/LazyState.swift diff --git a/Sources/OpenSwiftUI/Data/State/LazyState.swift b/Sources/OpenSwiftUI/Data/State/LazyState.swift new file mode 100644 index 000000000..c8a5533f0 --- /dev/null +++ b/Sources/OpenSwiftUI/Data/State/LazyState.swift @@ -0,0 +1,157 @@ +// +// LazyState.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: D23E5900647091DC07D54FF9E7D1D688 (SwiftUI) + +import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore + +// MARK: - LazyState + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +@propertyWrapper +@frozen +public struct LazyState: DynamicProperty { + @usableFromInline + enum Storage { + case thunk(() -> Value) + case value(Value) + + @usableFromInline + var value: Value { + switch self { + case let .thunk(thunk): + thunk() + case let .value(value): + value + } + } + } + + @usableFromInline + var _storage: Storage + + @usableFromInline + var _location: AnyLocation? + + public init(wrappedValue thunk: @autoclosure @escaping () -> Value) { + _storage = .thunk(thunk) + _location = nil + } + + public var wrappedValue: Value { + get { + getValue(forReading: true) + } + nonmutating set { + guard let _location else { + return + } + _location.set(newValue, transaction: Transaction()) + } + } + + public var projectedValue: Binding { + let value = getValue(forReading: false) + guard let _location else { + Log.runtimeIssues("Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.") + return .constant(value) + } + return Binding(value: value, location: _location) + } + + private func getValue(forReading: Bool) -> Value { + guard let _location else { + return _storage.value + } + if GraphHost.isUpdating { + if forReading { + _location.wasRead = true + } + return _storage.value + } else { + return _location.get() + } + } + + public static func _makeProperty( + in buffer: inout _DynamicPropertyBuffer, + container: _GraphValue, + fieldOffset: Int, + inputs: inout _GraphInputs + ) { + let attribute = Attribute(value: ()) + let box = LazyStatePropertyBox(signal: WeakAttribute(attribute)) + buffer.append(box, fieldOffset: fieldOffset) + addTreeValue( + attribute, + as: Value.self, + at: fieldOffset, + in: V.self, + flags: .stateSignal + ) + } +} + +@_spi(Private) +@available(*, unavailable) +extension LazyState: Sendable {} + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +extension LazyState where Value: ExpressibleByNilLiteral { + @inlinable + public init() { + self.init(wrappedValue: nil) + } +} + +@_spi(Private) +@available(*, unavailable) +extension LazyState.Storage: Sendable {} + +// MARK: - LazyStatePropertyBox + +private struct LazyStatePropertyBox: DynamicPropertyBox { + let signal: WeakAttribute + + var location: StoredLocation? + + typealias Property = LazyState + + func destroy() { + location?.invalidate() + } + + mutating func reset() { + location?.invalidate() + location = nil + } + + mutating func update(property: inout LazyState, phase: _GraphInputs.Phase) -> Bool { + let oldLocation = location + var changed = oldLocation == nil + let newLocation: StoredLocation + if let oldLocation { + newLocation = oldLocation + } else { + newLocation = property._location as? StoredLocation ?? StoredLocation( + initialValue: property._storage.value, + host: .currentHost, + signal: signal + ) + location = newLocation + } + let signalChanged = signal.changedValue()?.changed ?? false + property._storage = .value(newLocation.updateValue) + property._location = newLocation + if signalChanged { + changed = oldLocation == nil || newLocation.wasRead + } + return changed + } +} diff --git a/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift b/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift index 4ece458c9..9247bf6d6 100644 --- a/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift +++ b/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift @@ -7,7 +7,7 @@ // ID: EBDC911C9EE054BAE3D86F947C24B7C3 (SwiftUI) // ID: 4F21368B1C1680817451AC25B55A8D48 (SwiftUICore) -import OpenAttributeGraphShims +package import OpenAttributeGraphShims import OpenSwiftUI_SPI // MARK: - StoredLocationBase @@ -18,14 +18,14 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke var savedValue: [Value] var cache: LocationProjectionCache } - + fileprivate struct BeginUpdate: GraphMutation { weak var box: StoredLocationBase? - + func apply() { box?.beginUpdate() } - + func combine(with mutation: Mutation) -> Bool { guard let otherBeginUpdate = mutation as? BeginUpdate, let box, @@ -40,12 +40,12 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke return true } } - + @AtomicBox private var data: Data var _wasRead: Bool - + package init(initialValue value: Value) { _wasRead = false _data = AtomicBox(wrappedValue: Data(currentValue: value, savedValue: [], cache: .init())) @@ -54,7 +54,7 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke // MARK: - final properties and methods - final var updateValue: Value { + final package var updateValue: Value { $data.access { data in data.savedValue.first ?? data.currentValue } @@ -64,14 +64,14 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke Binding(value: updateValue, location: self) } - private final func beginUpdate() { + final private func beginUpdate() { $data.access { data in _ = data.savedValue.removeFirst() } notifyObservers() } - final func invalidate() { + final package func invalidate() { $data.access { data in data.cache.reset() } @@ -79,10 +79,12 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke // MARK: - non-final methods - fileprivate var isValid: Bool { true } - + fileprivate var isValid: Bool { + true + } + // MARK: - abstract method - + fileprivate var isUpdating: Bool { _openSwiftUIBaseClassAbstractMethod() } @@ -90,31 +92,31 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke fileprivate func commit(transaction: Transaction, mutation: BeginUpdate) { _openSwiftUIBaseClassAbstractMethod() } - + fileprivate func notifyObservers() { _openSwiftUIBaseClassAbstractMethod() } - + // MARK: - AnyLocation - - override package final var wasRead: Bool { + + override final package var wasRead: Bool { get { _wasRead } set { _wasRead = newValue } } - - override package final func get() -> Value { + + override final package func get() -> Value { data.currentValue } - override package final func projecting( + override final package func projecting( _ projection: P - )-> AnyLocation where Value == P.Base { + ) -> AnyLocation where Value == P.Base { $data.access { data in data.cache.reference(for: projection, on: self) } } - override package final func set(_ value: Value, transaction: Transaction) { + override final package func set(_ value: Value, transaction: Transaction) { guard !isUpdating else { Log.runtimeIssues("Modifying state during view update, this will cause undefined behavior.") return @@ -145,7 +147,7 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke commit(transaction: transaction, mutation: update) } } - + override package func update() -> (Value, Bool) { _wasRead = true return (updateValue, true) @@ -154,31 +156,31 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke // MARK: - StoredLocation -package final class StoredLocation: StoredLocationBase, @unchecked Sendable { +final package class StoredLocation: StoredLocationBase, @unchecked Sendable { weak var host: GraphHost? @WeakAttribute var signal: Void? - - init(initialValue value: Value, host: GraphHost?, signal: WeakAttribute) { + + package init(initialValue value: Value, host: GraphHost?, signal: WeakAttribute) { self.host = host - self._signal = signal + _signal = signal super.init(initialValue: value) } - + override fileprivate var isValid: Bool { host?.isValid ?? false } - + override fileprivate var isUpdating: Bool { host?.isUpdating ?? false } - + override fileprivate func commit(transaction: Transaction, mutation: StoredLocationBase.BeginUpdate) { host?.asyncTransaction( transaction, mutation: mutation ) } - + override fileprivate func notifyObservers() { $signal?.invalidateValue() }