Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions Sources/OpenSwiftUI/Data/State/LazyState.swift
Original file line number Diff line number Diff line change
@@ -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<Value>: 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<Value>?

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<Value> {
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.")

@augmentcode augmentcode Bot Jun 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Log.runtimeIssues message here still says "Accessing State's value" even though this is LazyState; that can be confusing when diagnosing runtime warnings.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

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<V>(
in buffer: inout _DynamicPropertyBuffer,
container: _GraphValue<V>,
fieldOffset: Int,
inputs: inout _GraphInputs
) {
let attribute = Attribute(value: ())
let box = LazyStatePropertyBox<Value>(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<Value>: DynamicPropertyBox {
let signal: WeakAttribute<Void>

var location: StoredLocation<Value>?

typealias Property = LazyState<Value>

func destroy() {
location?.invalidate()
}

mutating func reset() {
location?.invalidate()
location = nil
}

mutating func update(property: inout LazyState<Value>, phase: _GraphInputs.Phase) -> Bool {
let oldLocation = location
var changed = oldLocation == nil
let newLocation: StoredLocation<Value>
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
}
}
62 changes: 32 additions & 30 deletions Sources/OpenSwiftUICore/Data/State/StoredLocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// ID: EBDC911C9EE054BAE3D86F947C24B7C3 (SwiftUI)
// ID: 4F21368B1C1680817451AC25B55A8D48 (SwiftUICore)

import OpenAttributeGraphShims
package import OpenAttributeGraphShims
import OpenSwiftUI_SPI

// MARK: - StoredLocationBase
Expand All @@ -18,14 +18,14 @@ package class StoredLocationBase<Value>: AnyLocation<Value>, Location, @unchecke
var savedValue: [Value]
var cache: LocationProjectionCache
}

fileprivate struct BeginUpdate: GraphMutation {
weak var box: StoredLocationBase<Value>?

func apply() {
box?.beginUpdate()
}

func combine<Mutation: GraphMutation>(with mutation: Mutation) -> Bool {
guard let otherBeginUpdate = mutation as? BeginUpdate,
let box,
Expand All @@ -40,12 +40,12 @@ package class StoredLocationBase<Value>: AnyLocation<Value>, 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()))
Expand All @@ -54,7 +54,7 @@ package class StoredLocationBase<Value>: AnyLocation<Value>, 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
}
Expand All @@ -64,57 +64,59 @@ package class StoredLocationBase<Value>: AnyLocation<Value>, 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()
}
}

// MARK: - non-final methods

fileprivate var isValid: Bool { true }

fileprivate var isValid: Bool {
true
}

// MARK: - abstract method

fileprivate var isUpdating: Bool {
_openSwiftUIBaseClassAbstractMethod()
}

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<P: Projection>(
override final package func projecting<P: Projection>(
_ projection: P
)-> AnyLocation<P.Projected> where Value == P.Base {
) -> AnyLocation<P.Projected> 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
Expand Down Expand Up @@ -145,7 +147,7 @@ package class StoredLocationBase<Value>: AnyLocation<Value>, Location, @unchecke
commit(transaction: transaction, mutation: update)
}
}

override package func update() -> (Value, Bool) {
_wasRead = true
return (updateValue, true)
Expand All @@ -154,31 +156,31 @@ package class StoredLocationBase<Value>: AnyLocation<Value>, Location, @unchecke

// MARK: - StoredLocation

package final class StoredLocation<Value>: StoredLocationBase<Value>, @unchecked Sendable {
final package class StoredLocation<Value>: StoredLocationBase<Value>, @unchecked Sendable {
weak var host: GraphHost?
@WeakAttribute var signal: Void?
init(initialValue value: Value, host: GraphHost?, signal: WeakAttribute<Void>) {

package init(initialValue value: Value, host: GraphHost?, signal: WeakAttribute<Void>) {
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<Value>.BeginUpdate) {
host?.asyncTransaction(
transaction,
mutation: mutation
)
}

override fileprivate func notifyObservers() {
$signal?.invalidateValue()
}
Expand Down
Loading