From ea93caa32fbc9a9e420ebdcb8555691c0004b15c Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 3 Oct 2025 21:38:59 +0800 Subject: [PATCH 1/4] Optimize Namespace documentation --- Sources/OpenSwiftUICore/Data/DynamicProperty/Namespace.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUICore/Data/DynamicProperty/Namespace.swift b/Sources/OpenSwiftUICore/Data/DynamicProperty/Namespace.swift index 924d52b0f..9a448cee0 100644 --- a/Sources/OpenSwiftUICore/Data/DynamicProperty/Namespace.swift +++ b/Sources/OpenSwiftUICore/Data/DynamicProperty/Namespace.swift @@ -2,13 +2,14 @@ // Namespace.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete // ID: 79F323039D8AB6E63210271E57AD5E86 (SwiftUICore) /// A dynamic property type that allows access to a namespace defined /// by the persistent identity of the object containing the property /// (e.g. a view). +@available(OpenSwiftUI_v2_0, *) @frozen @propertyWrapper public struct Namespace: DynamicProperty, Sendable { From 7750bb26f206925cabedde403ed7210452f92756 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 3 Oct 2025 21:58:56 +0800 Subject: [PATCH 2/4] Update State documentation --- .../OpenSwiftUICore/Data/State/State.swift | 187 ++++++++++++++++-- 1 file changed, 176 insertions(+), 11 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/State/State.swift b/Sources/OpenSwiftUICore/Data/State/State.swift index 197e08d09..868907c12 100644 --- a/Sources/OpenSwiftUICore/Data/State/State.swift +++ b/Sources/OpenSwiftUICore/Data/State/State.swift @@ -1,10 +1,11 @@ // // State.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for 3.5.2 +// Audited for 6.5.4 // Status: Complete -// ID: 08168374F4710A99DCB15B5E8768D632 +// ID: 08168374F4710A99DCB15B5E8768D632 (SwiftUI) +// ID: F6975D1F800AFE6093C23B3DBD777BCF (SwiftUICore) import OpenAttributeGraphShims @@ -40,17 +41,12 @@ import OpenAttributeGraphShims /// need access, either directly for read-only access, or as a binding for /// read-write access. You can safely mutate state properties from any thread. /// -/// > Note: If you need to store a reference type, like an instance of a class, -/// use a ``StateObject`` instead. -/// /// ### Share state with subviews /// /// If you pass a state property to a subview, OpenSwiftUI updates the subview /// any time the value changes in the container view, but the subview can't /// modify the value. To enable the subview to modify the state's stored value, -/// pass a ``Binding`` instead. You can get a binding to a state value by -/// accessing the state's ``projectedValue``, which you get by prefixing the -/// property name with a dollar sign (`$`). +/// pass a ``Binding`` instead. /// /// For example, you can remove the `isPlaying` state from the play button in /// the above example, and instead make the button take a binding: @@ -66,7 +62,9 @@ import OpenAttributeGraphShims /// } /// /// Then you can define a player view that declares the state and creates a -/// binding to the state using the dollar sign prefix: +/// binding to the state. Get the binding to the state value by accessing the +/// state's ``projectedValue``, which you get by prefixing the property name +/// with a dollar sign (`$`): /// /// struct PlayerView: View { /// @State private var isPlaying: Bool = false // Create the state here now. @@ -80,12 +78,179 @@ import OpenAttributeGraphShims /// } /// } /// -/// Like you do for a ``StateObject``, declare ``State`` as private to prevent +/// Like you do for a ``StateObject``, declare `State` as private to prevent /// setting it in a memberwise initializer, which can conflict with the storage /// management that OpenSwiftUI provides. Unlike a state object, always /// initialize state by providing a default value in the state's /// declaration, as in the above examples. Use state only for storage that's /// local to a view and its subviews. +/// +/// ### Store observable objects +/// +/// You can also store observable objects that you create with the +/// [Observable()](https://swiftpackageindex.com/openswiftuiproject/openobservation/main/documentation/openobservation/observable()) +/// macro in `State`; for example: +/// +/// @Observable +/// class Library { +/// var name = "My library of books" +/// // ... +/// } +/// +/// struct ContentView: View { +/// @State private var library = Library() +/// +/// var body: some View { +/// LibraryView(library: library) +/// } +/// } +/// +/// A `State` property always instantiates its default value when OpenSwiftUI +/// instantiates the view. For this reason, avoid side effects and +/// performance-intensive work when initializing the default value. For +/// example, if a view updates frequently, allocating a new default object each +/// time the view initializes can become expensive. Instead, you can defer the +/// creation of the object using the ``View/task(priority:_:)`` modifier, which +/// is called only once when the view first appears: +/// +/// struct ContentView: View { +/// @State private var library: Library? +/// +/// var body: some View { +/// LibraryView(library: library) +/// .task { +/// library = Library() +/// } +/// } +/// } +/// +/// Delaying the creation of the observable state object ensures that +/// unnecessary allocations of the object doesn't happen each time OpenSwiftUI +/// initializes the view. Using the ``View/task(priority:_:)`` modifier is also +/// an effective way to defer any other kind of work required to create the +/// initial state of the view, such as network calls or file access. +/// +/// > Note: It's possible to store an object that conforms to the +/// [ObservableObject](https://swiftpackageindex.com/openswiftuiproject/opencombine/main/documentation/opencombine/observableobject) +/// protocol in a `State` property. However the view will only update when +/// the reference to the object changes, such as when setting the property with +/// a reference to another object. The view will not update if any of the +/// object's published properties change. To track changes to both the reference +/// and the object's published properties, use ``StateObject`` instead of +/// ``State`` when storing the object. +/// +/// ### Share observable state objects with subviews +/// +/// To share an [Observable](https://swiftpackageindex.com/openswiftuiproject/openobservation/main/documentation/openobservation/observable) +/// object stored in `State` with a subview, pass the object reference to +/// the subview. OpenSwiftUI updates the subview anytime an observable property of +/// the object changes, but only when the subview's ``View/body`` reads the +/// property. For example, in the following code `BookView` updates each time +/// `title` changes but not when `isAvailable` changes: +/// +/// @Observable +/// class Book { +/// var title = "A sample book" +/// var isAvailable = true +/// } +/// +/// struct ContentView: View { +/// @State private var book = Book() +/// +/// var body: some View { +/// BookView(book: book) +/// } +/// } +/// +/// struct BookView: View { +/// var book: Book +/// +/// var body: some View { +/// Text(book.title) +/// } +/// } +/// +/// `State` properties provide bindings to their value. When storing an object, +/// you can get a ``Binding`` to that object, specifically the reference to the +/// object. This is useful when you need to change the reference stored in +/// state in some other subview, such as setting the reference to `nil`: +/// +/// struct ContentView: View { +/// @State private var book: Book? +/// +/// var body: some View { +/// DeleteBookView(book: $book) +/// .task { +/// book = Book() +/// } +/// } +/// } +/// +/// struct DeleteBookView: View { +/// @Binding var book: Book? +/// +/// var body: some View { +/// Button("Delete book") { +/// book = nil +/// } +/// } +/// } +/// +/// However, passing a ``Binding`` to an object stored in `State` isn't +/// necessary when you need to change properties of that object. For example, +/// you can set the properties of the object to new values in a subview by +/// passing the object reference instead of a binding to the reference: +/// +/// struct ContentView: View { +/// @State private var book = Book() +/// +/// var body: some View { +/// BookCheckoutView(book: book) +/// } +/// } +/// +/// struct BookCheckoutView: View { +/// var book: Book +/// +/// var body: some View { +/// Button(book.isAvailable ? "Check out book" : "Return book") { +/// book.isAvailable.toggle() +/// } +/// } +/// } +/// +/// If you need a binding to a specific property of the object, pass either the +/// binding to the object and extract bindings to specific properties where +/// needed, or pass the object reference and use the ``Bindable`` property +/// wrapper to create bindings to specific properties. For example, in the +/// following code `BookEditorView` wraps `book` with `@Bindable`. Then the +/// view uses the `$` syntax to pass to a ``TextField`` a binding to `title`: +/// +/// struct ContentView: View { +/// @State private var book = Book() +/// +/// var body: some View { +/// BookView(book: book) +/// } +/// } +/// +/// struct BookView: View { +/// let book: Book +/// +/// var body: some View { +/// BookEditorView(book: book) +/// } +/// } +/// +/// struct BookEditorView: View { +/// @Bindable var book: Book +/// +/// var body: some View { +/// TextField("Title", text: $book.title) +/// } +/// } +/// +@available(OpenSwiftUI_v1_0, *) @frozen @propertyWrapper public struct State { From c01d5f670533330c0e7f62958e8a23703e3eb85a Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 4 Oct 2025 01:19:44 +0800 Subject: [PATCH 3/4] Update StoredLocation --- .../Data/Location/Location.swift | 28 ++++--- .../Data/State/StoredLocation.swift | 83 +++++++++++-------- 2 files changed, 67 insertions(+), 44 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Location/Location.swift b/Sources/OpenSwiftUICore/Data/Location/Location.swift index e135f7299..d76522232 100644 --- a/Sources/OpenSwiftUICore/Data/Location/Location.swift +++ b/Sources/OpenSwiftUICore/Data/Location/Location.swift @@ -48,22 +48,30 @@ open class AnyLocation: AnyLocationBase, @unchecked Sendable { } @_spi(ForOpenSwiftUIOnly) - open func get() -> Value { _openSwiftUIBaseClassAbstractMethod() } - - @_spi(ForOpenSwiftUIOnly) - open func set(_ value: Value, transaction: Transaction) { _openSwiftUIBaseClassAbstractMethod() } - + open func get() -> Value { + _openSwiftUIBaseClassAbstractMethod() + } + @_spi(ForOpenSwiftUIOnly) open func projecting

(_ projection: P) -> AnyLocation where Value == P.Base, P: Projection { _openSwiftUIBaseClassAbstractMethod() } - + @_spi(ForOpenSwiftUIOnly) - open func update() -> (Value, Bool) { _openSwiftUIBaseClassAbstractMethod() } - + open func set(_ value: Value, transaction: Transaction) { + _openSwiftUIBaseClassAbstractMethod() + } + @_spi(ForOpenSwiftUIOnly) - open func isEqual(to other: AnyLocation) -> Bool { self === other } - + open func update() -> (Value, Bool) { + _openSwiftUIBaseClassAbstractMethod() + } + + @_spi(ForOpenSwiftUIOnly) + open func isEqual(to other: AnyLocation) -> Bool { + self === other + } + package override init() { super.init() } diff --git a/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift b/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift index b6e36f8dc..5f2e3082b 100644 --- a/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift +++ b/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift @@ -2,14 +2,16 @@ // StoredLocation.swift // OpenSwiftUICore // -// Audited for 6.0.87 -// Status: WIP -// ID: EBDC911C9EE054BAE3D86F947C24B7C3 (RELEASE_2021) -// ID: 4F21368B1C1680817451AC25B55A8D48 (RELEASE_2024) +// Audited for 6.5.4 +// Status: Complete +// ID: EBDC911C9EE054BAE3D86F947C24B7C3 (SwiftUI) +// ID: 4F21368B1C1680817451AC25B55A8D48 (SwiftUICore) import OpenAttributeGraphShims import OpenSwiftUI_SPI +// MARK: - StoredLocationBase + package class StoredLocationBase: AnyLocation, Location, @unchecked Sendable { private struct Data { var currentValue: Value @@ -32,9 +34,8 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke else { return false } - box.$data.access { data in - _ = data.savedValue.removeFirst() + _ = data.savedValue.removeLast() } return true } @@ -42,6 +43,7 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke @AtomicBox private var data: Data + var _wasRead: Bool package init(initialValue value: Value) { @@ -49,7 +51,32 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke _data = AtomicBox(wrappedValue: Data(currentValue: value, savedValue: [], cache: .init())) super.init() } - + + // MARK: - final properties and methods + + final var updateValue: Value { + $data.access { data in + data.savedValue.first ?? data.currentValue + } + } + + final var binding: Binding { + Binding(value: updateValue, location: self) + } + + private final func beginUpdate() { + data.savedValue.removeFirst() + notifyObservers() + } + + final func invalidate() { + $data.access { data in + data.cache.reset() + } + } + + // MARK: - non-final methods + fileprivate var isValid: Bool { true } // MARK: - abstract method @@ -76,7 +103,15 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke override package final func get() -> Value { data.currentValue } - + + override package final func projecting( + _ projection: P + )-> 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) { guard !isUpdating else { Log.runtimeIssues("Modifying state during view update, this will cause undefined behavior.") @@ -99,49 +134,31 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke guard shouldCommit else { return } - var newTransaction = transaction.current + let transaction = transaction.current onMainThread { [weak self] in guard let self else { return } let update = BeginUpdate(box: self) - commit(transaction: newTransaction, mutation: update) + commit(transaction: transaction, mutation: update) } } - override package final func projecting(_ projection: P) -> AnyLocation where Value == P.Base { - data.cache.reference(for: projection, on: self) - } - override package func update() -> (Value, Bool) { _wasRead = true return (updateValue, true) } - - // MARK: - final properties and methods - - deinit { - } - - final var updateValue: Value { - $data.access { data in - data.savedValue.first ?? data.currentValue - } - } - - private final func beginUpdate() { - data.savedValue.removeFirst() - notifyObservers() - } } +// MARK: - StoredLocation + package final class StoredLocation: StoredLocationBase, @unchecked Sendable { weak var host: GraphHost? @WeakAttribute var signal: Void? init(initialValue value: Value, host: GraphHost?, signal: WeakAttribute) { self.host = host - _signal = signal + self._signal = signal super.init(initialValue: value) } @@ -156,9 +173,7 @@ package final class StoredLocation: StoredLocationBase, @unchecked override fileprivate func commit(transaction: Transaction, mutation: StoredLocationBase.BeginUpdate) { host?.asyncTransaction( transaction, - mutation: mutation, - style: .deferred, - mayDeferUpdate: true + mutation: mutation ) } From 0de2767f13894f7b4f8aabbc8aa04d2705385c22 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 4 Oct 2025 02:28:56 +0800 Subject: [PATCH 4/4] Update State implementation --- .../OpenSwiftUICore/Data/State/State.swift | 93 +++++++++++++------ .../Data/State/StoredLocation.swift | 6 +- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/State/State.swift b/Sources/OpenSwiftUICore/Data/State/State.swift index 868907c12..55a75975a 100644 --- a/Sources/OpenSwiftUICore/Data/State/State.swift +++ b/Sources/OpenSwiftUICore/Data/State/State.swift @@ -8,6 +8,9 @@ // ID: F6975D1F800AFE6093C23B3DBD777BCF (SwiftUICore) import OpenAttributeGraphShims +public import OpenObservation + +// MARK: State /// A property wrapper type that can read and write a value managed by OpenSwiftUI. /// @@ -253,7 +256,7 @@ import OpenAttributeGraphShims @available(OpenSwiftUI_v1_0, *) @frozen @propertyWrapper -public struct State { +public struct State: DynamicProperty { /// The current or initial (if box == nil) value of the state @usableFromInline var _value: Value @@ -292,7 +295,12 @@ public struct State { /// property. public init(wrappedValue value: Value) { _value = value - _location = nil + } + + @available(OpenSwiftUI_v5_0, *) + @usableFromInline + init(wrappedValue thunk: @autoclosure @escaping () -> Value) where Value: AnyObject, Value: Observable { + _value = thunk() } /// Creates a state property that stores an initial value. @@ -373,20 +381,7 @@ public struct State { } return Binding(value: value, location: _location) } -} -extension State where Value: ExpressibleByNilLiteral { - /// Creates a state property without an initial value. - /// - /// This initializer behaves like the ``init(wrappedValue:)`` initializer - /// with an input of `nil`. See that initializer for more information. - @inlinable - public init() { - self.init(wrappedValue: nil) - } -} - -extension State { private func getValue(forReading: Bool) -> Value { guard let _location else { return _value @@ -400,43 +395,83 @@ extension State { return _location.get() } } -} -extension State: DynamicProperty { public static func _makeProperty( in buffer: inout _DynamicPropertyBuffer, - container _: _GraphValue, + container: _GraphValue, fieldOffset: Int, - inputs _: inout _GraphInputs + inputs: inout _GraphInputs ) { let attribute = Attribute(value: ()) let box = StatePropertyBox(signal: WeakAttribute(attribute)) buffer.append(box, fieldOffset: fieldOffset) + addTreeValue( + attribute, + as: Value.self, + at: fieldOffset, + in: V.self, + flags: .stateSignal + ) } } +@available(OpenSwiftUI_v1_0, *) +extension State: Sendable where Value: Sendable {} + +@available(OpenSwiftUI_v1_0, *) +extension State where Value: ExpressibleByNilLiteral { + /// Creates a state property without an initial value. + /// + /// This initializer behaves like the ``init(wrappedValue:)`` initializer + /// with an input of `nil`. See that initializer for more information. + @inlinable + public init() { + self.init(wrappedValue: nil) + } +} + +// MARK: - StatePropertyBox + private struct StatePropertyBox: DynamicPropertyBox { let signal: WeakAttribute + var location: StoredLocation? typealias Property = State - func destroy() {} - mutating func reset() { location = nil } + + func destroy() { + location?.invalidate() + } + + mutating func reset() { + location?.invalidate() + location = nil + } + mutating func update(property: inout State, phase: _GraphInputs.Phase) -> Bool { - let locationChanged = location == nil - if location == nil { - location = property._location as? StoredLocation ?? StoredLocation( + let oldLocation = location + var changed = oldLocation == nil + let newLocation: StoredLocation + if let oldLocation { + newLocation = oldLocation + } else { + newLocation = property._location as? StoredLocation ?? StoredLocation( initialValue: property._value, host: .currentHost, signal: signal ) + location = newLocation } let signalChanged = signal.changedValue()?.changed ?? false - property._value = location!.updateValue - property._location = location! - return (signalChanged ? location!.wasRead : false) || locationChanged + property._value = newLocation.updateValue + property._location = newLocation + if signalChanged { + changed = oldLocation == nil || newLocation.wasRead + } + return changed } - func getState(type _: V.Type) -> Binding? { + + func getState(type: V.Type) -> Binding? { guard Value.self == V.self, let location else { @@ -444,6 +479,6 @@ private struct StatePropertyBox: DynamicPropertyBox { } let value = location.get() let binding = Binding(value: value, location: location) - return binding as? Binding + return (binding as! Binding) } } diff --git a/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift b/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift index 5f2e3082b..79d7e248b 100644 --- a/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift +++ b/Sources/OpenSwiftUICore/Data/State/StoredLocation.swift @@ -35,7 +35,7 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke return false } box.$data.access { data in - _ = data.savedValue.removeLast() + _ = data.savedValue.removeFirst() } return true } @@ -65,7 +65,9 @@ package class StoredLocationBase: AnyLocation, Location, @unchecke } private final func beginUpdate() { - data.savedValue.removeFirst() + $data.access { data in + _ = data.savedValue.removeFirst() + } notifyObservers() }