From 9abc85191c312982f29c33b2974bfb7488b28b90 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 4 Oct 2025 18:44:07 +0800 Subject: [PATCH 1/8] Update Binding documentation --- .../Data/Binding/Binding.swift | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Binding/Binding.swift b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift index f98d32755..57cdd3390 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/Binding.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift @@ -2,8 +2,8 @@ // Binding.swift // OpenSwiftUICore // -// Audited for 3.5.2 -// Status: Complete +// Audited for 6.5.4 +// Status: WIP // ID: 5436F2B399369BE3B016147A5F8FE9F2 (SwiftUI) // ID: C453EE81E759852CCC6400C47D93A43E (SwiftUICore) @@ -52,14 +52,24 @@ /// Whenever the user taps the `PlayButton`, the `PlayerView` updates its /// `isPlaying` state. /// +/// A binding conforms to ``Sendable`` only if its wrapped value type also +/// conforms to ``Sendable``. It is always safe to pass a sendable binding +/// between different concurrency domains. However, reading from or writing +/// to a binding's wrapped value from a different concurrency domain may or +/// may not be safe, depending on how the binding was created. OpenSwiftUI will +/// issue a warning at runtime if it detects a binding being used in a way +/// that may compromise data safety. +/// /// > Note: To create bindings to properties of a type that conforms to the /// [Observable](https://swiftpackageindex.com/openswiftuiproject/openobservation/main/documentation/openobservation/observable) /// protocol, use the ``Bindable`` property wrapper. For more information, /// see . -@frozen +/// +@available(OpenSwiftUI_v1_0, *) @propertyWrapper @dynamicMemberLookup public struct Binding { + /// The binding's transaction. /// /// The transaction captures the information needed to update the view when @@ -68,10 +78,26 @@ public struct Binding { package var location: AnyLocation - var _value: Value + package var _value: Value /// Creates a binding with closures that read and write the binding value. /// + /// A binding conforms to Sendable only if its wrapped value type also + /// conforms to Sendable. It is always safe to pass a sendable binding + /// between different concurrency domains. However, reading from or writing + /// to a binding's wrapped value from a different concurrency domain may or + /// may not be safe, depending on how the binding was created. OpenSwiftUI will + /// issue a warning at runtime if it detects a binding being used in a way + /// that may compromise data safety. + /// + /// For a "computed" binding created using get and set closure parameters, + /// the safety of accessing its wrapped value from a different concurrency + /// domain depends on whether those closure arguments are isolated to + /// a specific actor. For example, a computed binding with closure arguments + /// that are known (or inferred) to be isolated to the main actor must only + /// ever access its wrapped value on the main actor as well, even if the + /// binding is also sendable. + /// /// - Parameters: /// - get: A closure that retrieves the binding value. The closure has no /// parameters, and returns a value. @@ -87,6 +113,22 @@ public struct Binding { /// Creates a binding with a closure that reads from the binding value, and /// a closure that applies a transaction when writing to the binding value. /// + /// A binding conforms to Sendable only if its wrapped value type also + /// conforms to Sendable. It is always safe to pass a sendable binding + /// between different concurrency domains. However, reading from or writing + /// to a binding's wrapped value from a different concurrency domain may or + /// may not be safe, depending on how the binding was created. OpenSwiftUI will + /// issue a warning at runtime if it detects a binding being used in a way + /// that may compromise data safety. + /// + /// For a "computed" binding created using get and set closure parameters, + /// the safety of accessing its wrapped value from a different concurrency + /// domain depends on whether those closure arguments are isolated to + /// a specific actor. For example, a computed binding with closure arguments + /// that are known (or inferred) to be isolated to the main actor must only + /// ever access its wrapped value on the main actor as well, even if the + /// binding is also sendable. + /// /// - Parameters: /// - get: A closure to retrieve the binding value. The closure has no /// parameters, and returns a value. @@ -174,7 +216,7 @@ public struct Binding { public init(projectedValue: Binding) { self = projectedValue } - + /// Returns a binding to the resulting value of a given key path. /// /// - Parameter keyPath: A key path to a specific resulting value. @@ -186,6 +228,7 @@ public struct Binding { } extension Binding { + /// Creates a binding by projecting the base value to an optional value. /// /// - Parameter base: A value to project to an optional value. From 6515db2506a493c91bac0a2a2b1003f7b40316fc Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 5 Oct 2025 02:10:59 +0800 Subject: [PATCH 2/8] Update Location --- .../Data/Location/ConstantLocation.swift | 27 --- .../Data/Location/FunctionalLocation.swift | 23 --- .../Data/Location/Location.swift | 180 ++++++++++++++---- .../Data/Location/ConstantLocationTests.swift | 22 --- .../Location/FunctionalLocationTests.swift | 30 --- .../Data/Location/LocationTests.swift | 30 +++ 6 files changed, 172 insertions(+), 140 deletions(-) delete mode 100644 Sources/OpenSwiftUICore/Data/Location/ConstantLocation.swift delete mode 100644 Sources/OpenSwiftUICore/Data/Location/FunctionalLocation.swift delete mode 100644 Tests/OpenSwiftUICoreTests/Data/Location/ConstantLocationTests.swift delete mode 100644 Tests/OpenSwiftUICoreTests/Data/Location/FunctionalLocationTests.swift diff --git a/Sources/OpenSwiftUICore/Data/Location/ConstantLocation.swift b/Sources/OpenSwiftUICore/Data/Location/ConstantLocation.swift deleted file mode 100644 index c2260c9f7..000000000 --- a/Sources/OpenSwiftUICore/Data/Location/ConstantLocation.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ConstantLocation.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -import OpenAttributeGraphShims - -struct ConstantLocation: Location { - var value: Value - - init(value: Value) { - self.value = value - } - - var wasRead: Bool { - get { true } - nonmutating set {} - } - func get() -> Value { value } - func set(_: Value, transaction _: Transaction) {} - - static func == (lhs: ConstantLocation, rhs: ConstantLocation) -> Bool { - compareValues(lhs.value, rhs.value) - } -} diff --git a/Sources/OpenSwiftUICore/Data/Location/FunctionalLocation.swift b/Sources/OpenSwiftUICore/Data/Location/FunctionalLocation.swift deleted file mode 100644 index d3e0377ec..000000000 --- a/Sources/OpenSwiftUICore/Data/Location/FunctionalLocation.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// FunctionalLocation.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -import OpenAttributeGraphShims - -struct FunctionalLocation: Location { - var getValue: () -> Value - var setValue: (Value, Transaction) -> Void - var wasRead: Bool { - get { true } - nonmutating set {} - } - func get() -> Value { getValue() } - func set(_ value: Value, transaction: Transaction) { setValue(value, transaction) } - - static func == (lhs: FunctionalLocation, rhs: FunctionalLocation) -> Bool { - compareValues(lhs.getValue(), rhs.getValue()) - } -} diff --git a/Sources/OpenSwiftUICore/Data/Location/Location.swift b/Sources/OpenSwiftUICore/Data/Location/Location.swift index 972c0218d..fd1164b0b 100644 --- a/Sources/OpenSwiftUICore/Data/Location/Location.swift +++ b/Sources/OpenSwiftUICore/Data/Location/Location.swift @@ -2,9 +2,11 @@ // Location.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete -// ID: 3C10A6E9BB0D4644A364890A9BD57D68 +// ID: 3C10A6E9BB0D4644A364890A9BD57D68 (SwiftUICore) + +import OpenAttributeGraphShims // MARK: - Location @@ -25,8 +27,13 @@ extension Location { // MARK: - AnyLocationBase /// The base type of all type-erased locations. +@available(OpenSwiftUI_v1_0, *) @_documentation(visibility: private) -open class AnyLocationBase {} +open class AnyLocationBase { + init() { + _openSwiftUIEmptyStub() + } +} @available(*, unavailable) extension AnyLocationBase: Sendable {} @@ -39,6 +46,7 @@ extension AnyLocationBase: Sendable {} /// also the user types' responsibility to ensure that `get`, and `set` does /// not access the graph concurrently (`get` should not be called while graph /// is updating, for example). +@available(OpenSwiftUI_v1_0, *) @_documentation(visibility: private) open class AnyLocation: AnyLocationBase, @unchecked Sendable { @_spi(ForOpenSwiftUIOnly) @@ -58,7 +66,7 @@ open class AnyLocation: AnyLocationBase, @unchecked Sendable { } @_spi(ForOpenSwiftUIOnly) - open func set(_ value: Value, transaction: Transaction) { + open func set(_ newValue: Value, transaction: Transaction) { _openSwiftUIBaseClassAbstractMethod() } @@ -77,6 +85,7 @@ open class AnyLocation: AnyLocationBase, @unchecked Sendable { } } +@available(OpenSwiftUI_v5_0, *) extension AnyLocation: Equatable { public static func == (lhs: AnyLocation, rhs: AnyLocation) -> Bool { lhs.isEqual(to: rhs) @@ -160,8 +169,134 @@ package struct LocationProjectionCache { } } +// MARK: - FlattenedCollectionLocation + +package struct FlattenedCollectionLocation: Location where Base: Collection, Base: Equatable, Base.Element: AnyLocation { + package let base: Base + + package init(base: [AnyLocation]) { + self.base = base as! Base + } + + private var primaryLocation: Base.Element { base.first! } + + package var wasRead: Bool { + get { primaryLocation.wasRead } + set { primaryLocation.wasRead = newValue } + } + + package func get() -> Value { + primaryLocation.get() + } + + package func set(_ newValue: Value, transaction: Transaction) { + for location in base { + location.set(newValue, transaction: transaction) + } + } + + package func update() -> (Value, Bool) { + primaryLocation.update() + } +} + +// MARK: - ZipLocation + +package struct ZipLocation: Location { + package let locations: (AnyLocation, AnyLocation) + + package init(locations: (AnyLocation, AnyLocation)) { + self.locations = locations + } + + package var wasRead: Bool { + get { locations.0.wasRead || locations.1.wasRead } + set { + locations.0.wasRead = newValue + locations.1.wasRead = newValue + } + } + + package func get() -> (A, B) { + (locations.0.get(), locations.1.get()) + } + + package func set(_ newValue: (A, B), transaction: Transaction) { + locations.0.set(newValue.0, transaction: transaction) + locations.1.set(newValue.1, transaction: transaction) + } + + package func update() -> ((A, B), Bool) { + let (a, aChanged) = locations.0.update() + let (b, bChanged) = locations.1.update() + return ((a, b), aChanged || bChanged) + } + + package static func == (lhs: ZipLocation, rhs: ZipLocation) -> Bool { + lhs.locations == rhs.locations + } +} + +// MARK: - ConstantLocation + +package struct ConstantLocation: Location { + package var value: Value + + package init(value: Value) { + self.value = value + } + + package var wasRead: Bool { + get { true } + nonmutating set {} + } + + package func get() -> Value { value } + + package func set(_: Value, transaction _: Transaction) {} + + package static func == (lhs: ConstantLocation, rhs: ConstantLocation) -> Bool { + compareValues(lhs.value, rhs.value) + } +} + +// MARK: - FunctionalLocation + +package struct FunctionalLocation: Location { + package struct Functions { + package var getValue: () -> Value + package var setValue: (Value, Transaction) -> Void + } + + package var functions: Functions + + package init(getValue: @escaping () -> Value, setValue: @escaping (Value, Transaction) -> Void) { + self.functions = .init(getValue: getValue, setValue: setValue) + } + + package var wasRead: Bool { + get { true } + nonmutating set {} + } + + package func get() -> Value { + functions.getValue() + } + + package func set(_ newValue: Value, transaction: Transaction) { + functions.setValue(newValue, transaction) + } + + package static func == (lhs: FunctionalLocation, rhs: FunctionalLocation) -> Bool { + compareValues(lhs, rhs) + } +} + +// MARK: - ProjectedLocation + private struct ProjectedLocation: Location where P.Base == L.Value { var location: L + var projection: P init(location: L, projection: P) { @@ -176,7 +311,9 @@ private struct ProjectedLocation: Location where P.B set { location.wasRead = newValue } } - func get() -> Value { projection.get(base: location.get()) } + func get() -> Value { + projection.get(base: location.get()) + } func set(_ value: Value, transaction _: Transaction) { var base = location.get() @@ -189,36 +326,3 @@ private struct ProjectedLocation: Location where P.B return (value, result) } } - -// MARK: - FlattenedCollectionLocation - -package struct FlattenedCollectionLocation: Location where Base: Collection, Base: Equatable, Base.Element: AnyLocation { - package typealias Value = Value - - package let base: Base - - package init(base: [AnyLocation]) { - self.base = base as! Base - } - - private var primaryLocation: Base.Element { base.first! } - - package var wasRead: Bool { - get { primaryLocation.wasRead } - set { primaryLocation.wasRead = newValue } - } - - package func get() -> Value { - primaryLocation.get() - } - - package func set(_ newValue: Value, transaction: Transaction) { - for location in base { - location.set(newValue, transaction: transaction) - } - } - - package func update() -> (Value, Bool) { - primaryLocation.update() - } -} diff --git a/Tests/OpenSwiftUICoreTests/Data/Location/ConstantLocationTests.swift b/Tests/OpenSwiftUICoreTests/Data/Location/ConstantLocationTests.swift deleted file mode 100644 index e78d3c7de..000000000 --- a/Tests/OpenSwiftUICoreTests/Data/Location/ConstantLocationTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ConstantLocationTests.swift -// -// -// Created by Kyle on 2023/11/8. -// - -@testable import OpenSwiftUICore -import Testing - -struct ConstantLocationTests { - @Test - func constantLocation() throws { - let location = ConstantLocation(value: 0) - #expect(location.wasRead == true) - #expect(location.get() == 0) - location.wasRead = false - location.set(1, transaction: .init()) - #expect(location.wasRead == true) - #expect(location.get() == 0) - } -} diff --git a/Tests/OpenSwiftUICoreTests/Data/Location/FunctionalLocationTests.swift b/Tests/OpenSwiftUICoreTests/Data/Location/FunctionalLocationTests.swift deleted file mode 100644 index 698648ac0..000000000 --- a/Tests/OpenSwiftUICoreTests/Data/Location/FunctionalLocationTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// FunctionalLocationTests.swift -// -// -// Created by Kyle on 2023/11/8. -// - -@testable import OpenSwiftUICore -import Testing - -struct FunctionalLocationTests { - @Test - func functionalLocation() { - class V { - var count = 0 - } - let value = V() - let location = FunctionalLocation { - value.count - } setValue: { newCount, _ in - value.count = newCount * newCount - } - #expect(location.wasRead == true) - #expect(location.get() == 0) - location.wasRead = false - location.set(2, transaction: .init()) - #expect(location.wasRead == true) - #expect(location.get() == 4) - } -} diff --git a/Tests/OpenSwiftUICoreTests/Data/Location/LocationTests.swift b/Tests/OpenSwiftUICoreTests/Data/Location/LocationTests.swift index 74a3b348b..3462477a5 100644 --- a/Tests/OpenSwiftUICoreTests/Data/Location/LocationTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/Location/LocationTests.swift @@ -107,4 +107,34 @@ struct LocationTests { #expect(box.cache.cache.isEmpty == true) } #endif + + @Test + func constantLocation() throws { + let location = ConstantLocation(value: 0) + #expect(location.wasRead == true) + #expect(location.get() == 0) + location.wasRead = false + location.set(1, transaction: .init()) + #expect(location.wasRead == true) + #expect(location.get() == 0) + } + + @Test + func functionalLocation() { + class V { + var count = 0 + } + let value = V() + let location = FunctionalLocation { + value.count + } setValue: { newCount, _ in + value.count = newCount * newCount + } + #expect(location.wasRead == true) + #expect(location.get() == 0) + location.wasRead = false + location.set(2, transaction: .init()) + #expect(location.wasRead == true) + #expect(location.get() == 4) + } } From d574d9bd90ca6d3ba593f972e9e0dfecafa6aefd Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 5 Oct 2025 02:13:38 +0800 Subject: [PATCH 3/8] Add Location documentation --- .../Data/Location/Location.swift | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/Sources/OpenSwiftUICore/Data/Location/Location.swift b/Sources/OpenSwiftUICore/Data/Location/Location.swift index fd1164b0b..399dec7b8 100644 --- a/Sources/OpenSwiftUICore/Data/Location/Location.swift +++ b/Sources/OpenSwiftUICore/Data/Location/Location.swift @@ -10,11 +10,31 @@ import OpenAttributeGraphShims // MARK: - Location +/// A protocol representing a location that stores and manages a value with transaction support. +/// +/// `Location` types provide a unified interface for reading and writing values +/// with optional change tracking through transactions. package protocol Location: Equatable { associatedtype Value + + /// Indicates whether the location has been read. var wasRead: Bool { get set } + + /// Retrieves the current value from the location. + /// + /// - Returns: The current value stored at this location. func get() -> Value + + /// Sets a new value at the location within a transaction. + /// + /// - Parameters: + /// - value: The new value to store. + /// - transaction: The transaction context for the update. func set(_ value: Value, transaction: Transaction) + + /// Updates and retrieves the current value with change status. + /// + /// - Returns: A tuple containing the current value and a boolean indicating whether the value changed. func update() -> (Value, Bool) } @@ -97,6 +117,10 @@ extension AnyLocation: Sendable {} // MARK: - LocationBox +/// A type-erased wrapper that boxes a location for polymorphic storage and usage. +/// +/// `LocationBox` allows different location types to be stored and used through +/// a common interface while maintaining type safety for the value type. final package class LocationBox: AnyLocation, Location, @unchecked Sendable where L: Location { final private(set) package var location: L @@ -145,9 +169,19 @@ final package class LocationBox: AnyLocation, Location, @unchecked S // MARK: - LocationProjectionCache +/// A cache for projected locations to avoid recreating them on repeated access. +/// +/// This cache stores weak references to projected locations, allowing them to be +/// reused efficiently when the same projection is applied multiple times. package struct LocationProjectionCache { var cache: [AnyHashable: WeakBox] + /// Retrieves or creates a projected location for the given projection and base location. + /// + /// - Parameters: + /// - projection: The projection to apply. + /// - location: The base location to project from. + /// - Returns: A type-erased location containing the projected value. package mutating func reference(for projection: P, on location: L) -> AnyLocation where P: Projection, L: Location, P.Base == L.Value { if let box = cache[projection], let base = box.base, @@ -160,6 +194,8 @@ package struct LocationProjectionCache { return box } } + + /// Clears all cached projected locations. package mutating func reset() { cache = [:] } @@ -171,9 +207,17 @@ package struct LocationProjectionCache { // MARK: - FlattenedCollectionLocation +/// A location that aggregates multiple locations, using the first as primary for reads. +/// +/// When setting values, all locations in the collection are updated. This is useful +/// for scenarios where multiple locations need to be kept in sync. package struct FlattenedCollectionLocation: Location where Base: Collection, Base: Equatable, Base.Element: AnyLocation { + /// The collection of locations being aggregated. package let base: Base + /// Creates a flattened collection location from an array of locations. + /// + /// - Parameter base: The array of locations to aggregate. package init(base: [AnyLocation]) { self.base = base as! Base } @@ -202,9 +246,17 @@ package struct FlattenedCollectionLocation: Location where Base: Co // MARK: - ZipLocation +/// A location that combines two locations into a single tuple-valued location. +/// +/// `ZipLocation` allows treating two independent locations as a single location +/// with a tuple value, coordinating reads and writes across both. package struct ZipLocation: Location { + /// The pair of locations being combined. package let locations: (AnyLocation, AnyLocation) + /// Creates a zipped location from two locations. + /// + /// - Parameter locations: A tuple containing the two locations to combine. package init(locations: (AnyLocation, AnyLocation)) { self.locations = locations } @@ -239,9 +291,17 @@ package struct ZipLocation: Location { // MARK: - ConstantLocation +/// A location that always returns a constant value and ignores writes. +/// +/// `ConstantLocation` is useful for providing a location interface to immutable data +/// or default values that should not be modified. package struct ConstantLocation: Location { + /// The constant value stored in this location. package var value: Value + /// Creates a constant location with the specified value. + /// + /// - Parameter value: The constant value to store. package init(value: Value) { self.value = value } @@ -262,14 +322,28 @@ package struct ConstantLocation: Location { // MARK: - FunctionalLocation +/// A location implemented using custom getter and setter functions. +/// +/// `FunctionalLocation` provides maximum flexibility by allowing arbitrary +/// logic for reading and writing values through function closures. package struct FunctionalLocation: Location { + /// The functions used to implement location operations. package struct Functions { + /// The function to retrieve the current value. package var getValue: () -> Value + + /// The function to set a new value with a transaction. package var setValue: (Value, Transaction) -> Void } + /// The functions implementing this location's behavior. package var functions: Functions + /// Creates a functional location with the specified getter and setter. + /// + /// - Parameters: + /// - getValue: A closure that returns the current value. + /// - setValue: A closure that sets a new value within a transaction. package init(getValue: @escaping () -> Value, setValue: @escaping (Value, Transaction) -> Void) { self.functions = .init(getValue: getValue, setValue: setValue) } From 64d6bcc9e859190c4d78e33e7e7bcab4d1b2f94e Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 5 Oct 2025 12:24:29 +0800 Subject: [PATCH 4/8] Update Binding implementation --- .../Data/Binding/Binding.swift | 186 ++++++++++++------ .../Data/Binding/BindingOperations.swift | 8 + .../Data/Location/Location.swift | 1 + 3 files changed, 133 insertions(+), 62 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Binding/Binding.swift b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift index 57cdd3390..3025e0333 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/Binding.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift @@ -80,6 +80,21 @@ public struct Binding { package var _value: Value + package init(value: Value, location: AnyLocation, transaction: Transaction) { + self.transaction = transaction + self.location = location + self._value = value + } + + package init(value: Value, location: AnyLocation) { + self.init(value: value, location: location, transaction: Transaction()) + } + + @usableFromInline + static func getIsolated(@_inheritActorContext _ get: @escaping @isolated(any) @Sendable () -> Value) -> () -> Value { + _openSwiftUIUnimplementedFailure() + } + /// Creates a binding with closures that read and write the binding value. /// /// A binding conforms to Sendable only if its wrapped value type also @@ -225,54 +240,38 @@ public struct Binding { public subscript(dynamicMember keyPath: WritableKeyPath) -> Binding { projecting(keyPath) } -} -extension Binding { - - /// Creates a binding by projecting the base value to an optional value. - /// - /// - Parameter base: A value to project to an optional value. - public init(_ base: Binding) where Value == V? { - self = base.projecting(BindingOperations.ToOptional()) - } - - /// Creates a binding by projecting the base value to an unwrapped value. - /// - /// - Parameter base: A value to project to an unwrapped value. - /// - /// - Returns: A new binding or `nil` when `base` is `nil`. - public init?(_ base: Binding) { - guard let _ = base.wrappedValue else { - return nil + private func readValue() -> Value { + if Update.threadIsUpdating { + location.wasRead = true + return _value + } else { + return location.get() } - self = base.projecting(BindingOperations.ForceUnwrapping()) - } - - /// Creates a binding by projecting the base value to a hashable value. - /// - /// - Parameters: - /// - base: A `Hashable` value to project to an `AnyHashable` value. - public init(_ base: Binding) where Value == AnyHashable { - self = base.projecting(BindingOperations.ToAnyHashable()) } } +@available(OpenSwiftUI_v1_0, *) +extension Binding: @unchecked Sendable where Value: Sendable {} + +// MARK: - Binding + Protocols + +@available(OpenSwiftUI_v3_0, *) extension Binding: Identifiable where Value: Identifiable { + /// The stable identity of the entity associated with this instance, /// corresponding to the `id` of the binding's wrapped value. public var id: Value.ID { wrappedValue.id } - - /// A type representing the stable identity of the entity associated with - /// an instance. - public typealias ID = Value.ID } +@available(OpenSwiftUI_v3_0, *) extension Binding: Sequence where Value: MutableCollection { public typealias Element = Binding public typealias Iterator = IndexingIterator> public typealias SubSequence = Slice> } +@available(OpenSwiftUI_v3_0, *) extension Binding: Collection where Value: MutableCollection { public typealias Index = Value.Index public typealias Indices = Value.Indices @@ -305,6 +304,7 @@ extension Binding: Collection where Value: MutableCollection { } } +@available(OpenSwiftUI_v3_0, *) extension Binding: BidirectionalCollection where Value: BidirectionalCollection, Value: MutableCollection { public func index(before index: Binding.Index) -> Binding.Index { wrappedValue.index(before: index) @@ -315,8 +315,12 @@ extension Binding: BidirectionalCollection where Value: BidirectionalCollection, } } +@available(OpenSwiftUI_v3_0, *) extension Binding: RandomAccessCollection where Value: MutableCollection, Value: RandomAccessCollection {} +// MARK: - Binding + Transaction / Animation + +@available(OpenSwiftUI_v1_0, *) extension Binding { /// Specifies a transaction for the binding. /// @@ -342,11 +346,57 @@ extension Binding { } } +// MARK: - Binding + Transform + +@available(OpenSwiftUI_v1_0, *) +extension Binding { + + package subscript( + keyPath: WritableKeyPath, + default defaultValue: Subject + ) -> Binding { + let projection = keyPath.composed( + with: BindingOperations.NilCoalescing( + defaultValue: defaultValue, + ) + ) + return projecting(projection) + } + + package func zip(with rhs: Binding) -> Binding<(Value, T)> { + let value = (self._value, rhs._value) + let box = LocationBox(ZipLocation(locations: (self.location, rhs.location))) + return Binding<(Value, T)>(value: value, location: box, transaction: transaction) + } + + package func projecting(_ p: P) -> Binding where P.Base == Value { + Binding( + value: p.get(base: _value), + location: location.projecting(p), + transaction: transaction + ) + } +} + +extension Binding { + package init(flattening source: some Collection>) { + let flattenLocation = FlattenedCollectionLocation]>(base: source.map(\.location)) + let value = flattenLocation.get() + let location = LocationBox(flattenLocation) + self.init(value: value, location: location) + } +} + +// MARK: - Binding + DynamicProperty + +@available(OpenSwiftUI_v1_0, *) extension Binding: DynamicProperty { + private struct ScopedLocation: Location { var base: AnyLocation + var wasRead: Bool - + init(base: AnyLocation) { self.base = base self.wasRead = base.wasRead @@ -363,61 +413,73 @@ extension Binding: DynamicProperty { func update() -> (Value, Bool) { base.update() } + + static func == (lhs: ScopedLocation, rhs: ScopedLocation) -> Bool { + lhs.base == rhs.base && lhs.wasRead == rhs.wasRead + } } private struct Box: DynamicPropertyBox { var location: LocationBox? - typealias Property = Binding - func destroy() {} - func reset() {} + typealias Property = Binding + mutating func update(property: inout Property, phase: _GraphInputs.Phase) -> Bool { - if let location { - if location.location.base !== property.location { - self.location = LocationBox(ScopedLocation(base: property.location)) - if location.wasRead { - self.location!.wasRead = true - } - } + let newLocation: LocationBox + if let location, location.location.base === property.location { + newLocation = location } else { - location = LocationBox(ScopedLocation(base: property.location)) + let wasRead = location?.wasRead ?? false + let box = LocationBox(ScopedLocation(base: property.location)) + location = box + if wasRead { + box.wasRead = wasRead + } + newLocation = box } - let (value, changed) = location!.update() - property.location = location! + let (value, changed) = newLocation.update() + property.location = newLocation property._value = value - return changed ? location!.wasRead : false + return changed && newLocation.wasRead } } public static func _makeProperty( in buffer: inout _DynamicPropertyBuffer, - container _: _GraphValue, + container: _GraphValue, fieldOffset: Int, - inputs _: inout _GraphInputs + inputs: inout _GraphInputs ) { buffer.append(Box(), fieldOffset: fieldOffset) } } -// MARK: - Binding Internal API - extension Binding { - package init(value: Value, location: AnyLocation, transaction: Transaction = Transaction()) { - self.transaction = transaction - self.location = location - self._value = value + + /// Creates a binding by projecting the base value to an optional value. + /// + /// - Parameter base: A value to project to an optional value. + public init(_ base: Binding) where Value == V? { + self = base.projecting(BindingOperations.ToOptional()) } - private func readValue() -> Value { - if GraphHost.isUpdating { - location.wasRead = true - return _value - } else { - return location.get() + /// Creates a binding by projecting the base value to an unwrapped value. + /// + /// - Parameter base: A value to project to an unwrapped value. + /// + /// - Returns: A new binding or `nil` when `base` is `nil`. + public init?(_ base: Binding) { + guard let _ = base.wrappedValue else { + return nil } + self = base.projecting(BindingOperations.ForceUnwrapping()) } - package func projecting(_ p: P) -> Binding where P.Base == Value { - Binding(value: p.get(base: _value), location: location.projecting(p), transaction: transaction) + /// Creates a binding by projecting the base value to a hashable value. + /// + /// - Parameters: + /// - base: A `Hashable` value to project to an `AnyHashable` value. + public init(_ base: Binding) where Value == AnyHashable { + self = base.projecting(BindingOperations.ToAnyHashable()) } } diff --git a/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift b/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift index cd4a7d9b2..b40be0833 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift @@ -7,6 +7,8 @@ enum BindingOperations {} +private var nilCoalescingGenerationCounter: Int = 0 + extension BindingOperations { struct ForceUnwrapping: Projection { func get(base: Value?) -> Value { base! } @@ -17,6 +19,12 @@ extension BindingOperations { let defaultValue: Value let generation: Int + init(defaultValue: Value) { + self.defaultValue = defaultValue + // TODO + self.generation = nilCoalescingGenerationCounter + } + func get(base: Value?) -> Value { base ?? defaultValue } func set(base: inout Value?, newValue: Value) { base = newValue } diff --git a/Sources/OpenSwiftUICore/Data/Location/Location.swift b/Sources/OpenSwiftUICore/Data/Location/Location.swift index 399dec7b8..c204ef44d 100644 --- a/Sources/OpenSwiftUICore/Data/Location/Location.swift +++ b/Sources/OpenSwiftUICore/Data/Location/Location.swift @@ -212,6 +212,7 @@ package struct LocationProjectionCache { /// When setting values, all locations in the collection are updated. This is useful /// for scenarios where multiple locations need to be kept in sync. package struct FlattenedCollectionLocation: Location where Base: Collection, Base: Equatable, Base.Element: AnyLocation { + /// The collection of locations being aggregated. package let base: Base From 6daffdb70fd383a8495984f7677bda80268bb75d Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 5 Oct 2025 13:55:45 +0800 Subject: [PATCH 5/8] Add EnableRuntimeConcurrencyCheck for Binding --- Package.resolved | 2 +- Package.swift | 7 + .../Data/Binding/Binding.swift | 205 +++++++++++++++++- 3 files changed, 204 insertions(+), 10 deletions(-) diff --git a/Package.resolved b/Package.resolved index d37295b3b..3ede887c1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "41ef3b0800cbdd92616dd3d7912e99c1d1063b38e82444df3eec7506bbf1053b", + "originHash" : "106a21412583c15dcfc5506af42f485123fabe106be5887207958028611f2bb0", "pins" : [ { "identity" : "darwinprivateframeworks", diff --git a/Package.swift b/Package.swift index c6ba1f6c0..54349789f 100644 --- a/Package.swift +++ b/Package.swift @@ -233,6 +233,13 @@ if enablePrivateImports { sharedSwiftSettings.append(.unsafeFlags(["-Xfrontend", "-enable-private-imports"])) } +// MARK: - [env] OPENSWIFTUI_ENABLE_RUNTIME_CONCURRENCY_CHECK + +let enableRuntimeConcurrencyCheck = envEnable("OPENSWIFTUI_ENABLE_RUNTIME_CONCURRENCY_CHECK", default: false) +if enableRuntimeConcurrencyCheck { + sharedSwiftSettings.append(.define("OPENSWIFTUI_ENABLE_RUNTIME_CONCURRENCY_CHECK")) +} + // MARK: - OpenSwiftUISPI Target let openSwiftUISPITarget = Target.target( diff --git a/Sources/OpenSwiftUICore/Data/Binding/Binding.swift b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift index 3025e0333..796956ebd 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/Binding.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift @@ -3,10 +3,14 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: WIP +// Status: Complete // ID: 5436F2B399369BE3B016147A5F8FE9F2 (SwiftUI) // ID: C453EE81E759852CCC6400C47D93A43E (SwiftUICore) +#if OPENSWIFTUI_ENABLE_RUNTIME_CONCURRENCY_CHECK +public import class Foundation.UserDefaults +#endif + /// A property wrapper type that can read and write a value owned by a source of /// truth. /// @@ -92,9 +96,21 @@ public struct Binding { @usableFromInline static func getIsolated(@_inheritActorContext _ get: @escaping @isolated(any) @Sendable () -> Value) -> () -> Value { - _openSwiftUIUnimplementedFailure() + { + let enableRuntimeCheck = false + let nonisolatedGet = get as () -> Value + return if let isolation = extractIsolation(get), + enableRuntimeCheck { + isolation.assumeIsolated { _ in + nonisolatedGet() + } + } else { + nonisolatedGet() + } + } } + #if OPENSWIFTUI_ENABLE_RUNTIME_CONCURRENCY_CHECK /// Creates a binding with closures that read and write the binding value. /// /// A binding conforms to Sendable only if its wrapped value type also @@ -119,7 +135,51 @@ public struct Binding { /// - set: A closure that sets the binding value. The closure has the /// following parameter: /// - newValue: The new value of the binding value. - public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) { + @_alwaysEmitIntoClient + public init( + @_inheritActorContext get: @escaping @isolated(any) @Sendable () -> Value, + @_inheritActorContext set: @escaping @isolated(any) @Sendable (Value) -> Void + ) { + self.init(isolatedGet: get, isolatedSet: set) + } + + @usableFromInline + @_transparent + init( + @_inheritActorContext isolatedGet: @escaping @isolated(any) @Sendable () -> Value, + @_inheritActorContext isolatedSet: @escaping @isolated(any) @Sendable (Value) -> Void, + ) { + let enableRuntimeCheck = UserDefaults.standard.bool( + forKey: "org.OpenSwiftUIProject.OpenSwiftUI.EnableRuntimeConcurrencyCheck", + ) + self.init( + get: { + let nonisolatedGet = isolatedGet as () -> Value + return if let isolation = extractIsolation(isolatedGet), + enableRuntimeCheck + { + isolation.assumeIsolated { _ in nonisolatedGet() } + } else { + nonisolatedGet() + } + }, + set: { value in + let nonisolatedSet = isolatedSet as (Value) -> Void + if let isolation = extractIsolation(isolatedSet), + enableRuntimeCheck + { + isolation.assumeIsolated { _ in + nonisolatedSet(value) + } + } else { + nonisolatedSet(value) + } + }, + ) + } + + @usableFromInline + init(get: @escaping () -> Value, set: @escaping (Value) -> Void) { let location = FunctionalLocation(getValue: get) { value, _ in set(value) } let box = LocationBox(location) self.init(value: get(), location: box) @@ -151,11 +211,129 @@ public struct Binding { /// following parameters: /// - newValue: The new value of the binding value. /// - transaction: The transaction to apply when setting a new value. - public init(get: @escaping () -> Value, set: @escaping (Value, Transaction) -> Void) { + @_alwaysEmitIntoClient + public init( + @_inheritActorContext get: @escaping @isolated(any) @Sendable () -> Value, + @_inheritActorContext set: @escaping @isolated(any) @Sendable (Value, Transaction) -> Void + ) { + self.init(isolatedGet: get, isolatedSet: set) + } + + @usableFromInline + @_transparent + init( + @_inheritActorContext isolatedGet: @escaping @isolated(any) @Sendable () -> Value, + @_inheritActorContext isolatedSet: @escaping @isolated(any) @Sendable (Value, Transaction) -> Void, + ) { + let enableRuntimeCheck = UserDefaults.standard.bool( + forKey: "org.OpenSwiftUIProject.OpenSwiftUI.EnableRuntimeConcurrencyCheck", + ) + self.init( + get: { + let nonisolatedGet = isolatedGet as () -> Value + return if let isolation = extractIsolation(isolatedGet), + enableRuntimeCheck + { + isolation.assumeIsolated { _ in nonisolatedGet() } + } else { + nonisolatedGet() + } + }, + set: { value, transaction in + let nonisolatedSet = isolatedSet as (Value, Transaction) -> Void + if let isolation = extractIsolation(isolatedSet), + enableRuntimeCheck + { + isolation.assumeIsolated { _ in + nonisolatedSet(value, transaction) + } + } else { + nonisolatedSet(value, transaction) + } + }, + ) + } + + @usableFromInline + init(get: @escaping () -> Value, set: @escaping (Value, Transaction) -> Void) { let location = FunctionalLocation(getValue: get, setValue: set) let box = LocationBox(location) self.init(value: get(), location: box) } + #else + /// Creates a binding with closures that read and write the binding value. + /// + /// A binding conforms to Sendable only if its wrapped value type also + /// conforms to Sendable. It is always safe to pass a sendable binding + /// between different concurrency domains. However, reading from or writing + /// to a binding's wrapped value from a different concurrency domain may or + /// may not be safe, depending on how the binding was created. OpenSwiftUI will + /// issue a warning at runtime if it detects a binding being used in a way + /// that may compromise data safety. + /// + /// For a "computed" binding created using get and set closure parameters, + /// the safety of accessing its wrapped value from a different concurrency + /// domain depends on whether those closure arguments are isolated to + /// a specific actor. For example, a computed binding with closure arguments + /// that are known (or inferred) to be isolated to the main actor must only + /// ever access its wrapped value on the main actor as well, even if the + /// binding is also sendable. + /// + /// - Parameters: + /// - get: A closure that retrieves the binding value. The closure has no + /// parameters, and returns a value. + /// - set: A closure that sets the binding value. The closure has the + /// following parameter: + /// - newValue: The new value of the binding value. + @preconcurrency + public init( + @_inheritActorContext get: @escaping @isolated(any) @Sendable () -> Value, + @_inheritActorContext set: @escaping @isolated(any) @Sendable (Value) -> Void + ) { + let nonisolatedGet = get as () -> Value + let nonisolatedSet = set as (Value) -> Void + let location = FunctionalLocation(getValue: nonisolatedGet) { value, _ in nonisolatedSet(value) } + let box = LocationBox(location) + self.init(value: nonisolatedGet(), location: box) + } + + /// Creates a binding with a closure that reads from the binding value, and + /// a closure that applies a transaction when writing to the binding value. + /// + /// A binding conforms to Sendable only if its wrapped value type also + /// conforms to Sendable. It is always safe to pass a sendable binding + /// between different concurrency domains. However, reading from or writing + /// to a binding's wrapped value from a different concurrency domain may or + /// may not be safe, depending on how the binding was created. OpenSwiftUI will + /// issue a warning at runtime if it detects a binding being used in a way + /// that may compromise data safety. + /// + /// For a "computed" binding created using get and set closure parameters, + /// the safety of accessing its wrapped value from a different concurrency + /// domain depends on whether those closure arguments are isolated to + /// a specific actor. For example, a computed binding with closure arguments + /// that are known (or inferred) to be isolated to the main actor must only + /// ever access its wrapped value on the main actor as well, even if the + /// binding is also sendable. + /// + /// - Parameters: + /// - get: A closure to retrieve the binding value. The closure has no + /// parameters, and returns a value. + /// - set: A closure to set the binding value. The closure has the + /// following parameters: + /// - newValue: The new value of the binding value. + /// - transaction: The transaction to apply when setting a new value. + public init( + @_inheritActorContext get: @escaping @isolated(any) @Sendable () -> Value, + @_inheritActorContext set: @escaping @isolated(any) @Sendable (Value, Transaction) -> Void + ) { + let nonisolatedGet = get as () -> Value + let nonisolatedSet = set as (Value, Transaction) -> Void + let location = FunctionalLocation(getValue: nonisolatedGet, setValue: nonisolatedSet) + let box = LocationBox(location) + self.init(value: nonisolatedGet(), location: box) + } + #endif /// Creates a binding with an immutable value. /// @@ -401,15 +579,15 @@ extension Binding: DynamicProperty { self.base = base self.wasRead = base.wasRead } - + func get() -> Value { base.get() } - + func set(_ value: Value, transaction: Transaction) { base.set(value, transaction: transaction) } - + func update() -> (Value, Bool) { base.update() } @@ -418,7 +596,7 @@ extension Binding: DynamicProperty { lhs.base == rhs.base && lhs.wasRead == rhs.wasRead } } - + private struct Box: DynamicPropertyBox { var location: LocationBox? @@ -443,7 +621,7 @@ extension Binding: DynamicProperty { return changed && newLocation.wasRead } } - + public static func _makeProperty( in buffer: inout _DynamicPropertyBuffer, container: _GraphValue, @@ -454,6 +632,15 @@ extension Binding: DynamicProperty { } } +// NOTE: This is currently not used +struct EnableRuntimeConcurrencyCheck: UserDefaultKeyedFeature { + static var key: String { "org.OpenSwiftUIProject.OpenSwiftUI.EnableRuntimeConcurrencyCheck" } + + static var cachedValue: Bool? + + static var isEnabled: Bool { true } +} + extension Binding { /// Creates a binding by projecting the base value to an optional value. From d38adf08024c4c87be9c16a60d7bf2698fa88bf5 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 5 Oct 2025 13:57:47 +0800 Subject: [PATCH 6/8] Update Location file location --- .../Binding+ObjectLocation.swift | 0 .../Data/{Location => Binding}/Location.swift | 0 .../Data/{Location => Binding}/Projection.swift | 17 ++++++++++------- .../{Location => Binding}/LocationTests.swift | 0 4 files changed, 10 insertions(+), 7 deletions(-) rename Sources/OpenSwiftUICore/Data/{Location => Binding}/Binding+ObjectLocation.swift (100%) rename Sources/OpenSwiftUICore/Data/{Location => Binding}/Location.swift (100%) rename Sources/OpenSwiftUICore/Data/{Location => Binding}/Projection.swift (86%) rename Tests/OpenSwiftUICoreTests/Data/{Location => Binding}/LocationTests.swift (100%) diff --git a/Sources/OpenSwiftUICore/Data/Location/Binding+ObjectLocation.swift b/Sources/OpenSwiftUICore/Data/Binding/Binding+ObjectLocation.swift similarity index 100% rename from Sources/OpenSwiftUICore/Data/Location/Binding+ObjectLocation.swift rename to Sources/OpenSwiftUICore/Data/Binding/Binding+ObjectLocation.swift diff --git a/Sources/OpenSwiftUICore/Data/Location/Location.swift b/Sources/OpenSwiftUICore/Data/Binding/Location.swift similarity index 100% rename from Sources/OpenSwiftUICore/Data/Location/Location.swift rename to Sources/OpenSwiftUICore/Data/Binding/Location.swift diff --git a/Sources/OpenSwiftUICore/Data/Location/Projection.swift b/Sources/OpenSwiftUICore/Data/Binding/Projection.swift similarity index 86% rename from Sources/OpenSwiftUICore/Data/Location/Projection.swift rename to Sources/OpenSwiftUICore/Data/Binding/Projection.swift index a6b6aee4d..eb3243bc8 100644 --- a/Sources/OpenSwiftUICore/Data/Location/Projection.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/Projection.swift @@ -2,10 +2,13 @@ // Projection.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete +// MARK: - Projection + @_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) public protocol Projection: Hashable { associatedtype Base associatedtype Projected @@ -20,13 +23,13 @@ extension Projection { } } +// MARK: - ComposedProjection + package struct ComposedProjection: Projection where Left: Projection, Right: Projection, Left.Projected == Right.Base { let left: Left + let right: Right - - package typealias Base = Left.Base - package typealias Projected = Right.Projected - + package func get(base: Left.Base) -> Right.Projected { right.get(base: left.get(base: base)) } @@ -39,9 +42,9 @@ package struct ComposedProjection: Projection where Left: Projectio } @_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) extension WritableKeyPath: Projection { - public typealias Base = Root - public typealias Projected = Value public func get(base: Root) -> Value { base[keyPath: self] } + public func set(base: inout Root, newValue: Value) { base[keyPath: self] = newValue } } diff --git a/Tests/OpenSwiftUICoreTests/Data/Location/LocationTests.swift b/Tests/OpenSwiftUICoreTests/Data/Binding/LocationTests.swift similarity index 100% rename from Tests/OpenSwiftUICoreTests/Data/Location/LocationTests.swift rename to Tests/OpenSwiftUICoreTests/Data/Binding/LocationTests.swift From 248a4f763fbea707b694e9745f7336987b906e33 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 5 Oct 2025 15:03:26 +0800 Subject: [PATCH 7/8] Update BindingOperations --- .../Data/Binding/Binding.swift | 30 ---- .../Data/Binding/BindingOperations.swift | 158 +++++++++++++++--- 2 files changed, 133 insertions(+), 55 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Binding/Binding.swift b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift index 796956ebd..337d2b6ed 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/Binding.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift @@ -640,33 +640,3 @@ struct EnableRuntimeConcurrencyCheck: UserDefaultKeyedFeature { static var isEnabled: Bool { true } } - -extension Binding { - - /// Creates a binding by projecting the base value to an optional value. - /// - /// - Parameter base: A value to project to an optional value. - public init(_ base: Binding) where Value == V? { - self = base.projecting(BindingOperations.ToOptional()) - } - - /// Creates a binding by projecting the base value to an unwrapped value. - /// - /// - Parameter base: A value to project to an unwrapped value. - /// - /// - Returns: A new binding or `nil` when `base` is `nil`. - public init?(_ base: Binding) { - guard let _ = base.wrappedValue else { - return nil - } - self = base.projecting(BindingOperations.ForceUnwrapping()) - } - - /// Creates a binding by projecting the base value to a hashable value. - /// - /// - Parameters: - /// - base: A `Hashable` value to project to an `AnyHashable` value. - public init(_ base: Binding) where Value == AnyHashable { - self = base.projecting(BindingOperations.ToAnyHashable()) - } -} diff --git a/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift b/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift index b40be0833..63f21a9e6 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift @@ -1,60 +1,168 @@ // // BindingOperations.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for 3.5.2 +// Audited for 6.5.4 // Status: Complete +// ID: 1B4A0A6DD72E915E1D833753C43AC6E0 (SwiftUICore) -enum BindingOperations {} +@available(OpenSwiftUI_v1_0, *) +extension Binding { + + /// Creates a binding by projecting the base value to an optional value. + /// + /// - Parameter base: A value to project to an optional value. + public init(_ base: Binding) where Value == V? { + self = base.projecting(BindingOperations.ToOptional()) + } + + /// Creates a binding by projecting the base value to an unwrapped value. + /// + /// - Parameter base: A value to project to an unwrapped value. + /// + /// - Returns: A new binding or `nil` when `base` is `nil`. + public init?(_ base: Binding) { + guard let _ = base.wrappedValue else { + return nil + } + self = base.projecting(BindingOperations.ForceUnwrapping()) + } + + /// Creates a binding by projecting the base value to a hashable value. + /// + /// - Parameters: + /// - base: A `Hashable` value to project to an `AnyHashable` value. + public init(_ base: Binding) where Value == AnyHashable, V: Hashable { + self = base.projecting(BindingOperations.ToAnyHashable()) + } + + package init(_ base: Binding) where Value == Double, V: BinaryFloatingPoint { + self = base.projecting(BindingOperations.ToDouble()) + } + + package static func == (lhs: Binding, rhs: Value) -> Binding where Value: Hashable { + lhs.projecting(BindingOperations.Equals(value: rhs)) + } +} + +private let _constantFalse: Binding = .constant(false) + +extension Binding where Value == Bool { + package static var `false`: Binding { + _constantFalse + } +} private var nilCoalescingGenerationCounter: Int = 0 -extension BindingOperations { - struct ForceUnwrapping: Projection { - func get(base: Value?) -> Value { base! } - func set(base: inout Value?, newValue: Value) { base = newValue } +package enum BindingOperations { + // MARK: - ToOptional + + package struct ToOptional: Projection { + package func get(base: Value) -> Value? { + base + } + + package func set(base: inout Value, newValue: Value?) { + guard let newValue else { + return + } + base = newValue + } } - struct NilCoalescing: Projection { + // MARK: - ToAnyHashable + + package struct ToAnyHashable: Projection { + package func get(base: Value) -> AnyHashable { + AnyHashable(base) + } + package func set(base: inout Value, newValue: AnyHashable) { + base = newValue.base as! Value + } + } + + package struct ForceUnwrapping: Projection { + package func get(base: Value?) -> Value { + base! + } + + package func set(base: inout Value?, newValue: Value) { + base = newValue + } + + package init() { + _openSwiftUIEmptyStub() + } + } + + package struct NilCoalescing: Projection { let defaultValue: Value let generation: Int - init(defaultValue: Value) { + package init(defaultValue: Value) { self.defaultValue = defaultValue - // TODO self.generation = nilCoalescingGenerationCounter + nilCoalescingGenerationCounter += 1 } - func get(base: Value?) -> Value { base ?? defaultValue } - func set(base: inout Value?, newValue: Value) { base = newValue } + package func get(base: Value?) -> Value { + base ?? defaultValue + } - static func == (lhs: BindingOperations.NilCoalescing, rhs: BindingOperations.NilCoalescing) -> Bool { + package func set(base: inout Value?, newValue: Value) { + base = newValue + } + + package static func == (lhs: BindingOperations.NilCoalescing, rhs: BindingOperations.NilCoalescing) -> Bool { lhs.generation == rhs.generation } - func hash(into hasher: inout Hasher) { + package func hash(into hasher: inout Hasher) { hasher.combine(generation) } } - struct ToAnyHashable: Projection { - func get(base: Value) -> AnyHashable { AnyHashable(base) } - func set(base: inout Value, newValue: AnyHashable) { base = newValue.base as! Value } + package struct ToDouble: Projection where Base: BinaryFloatingPoint { + package func get(base: Base) -> Double { + Double(base) + } + + package func set(base: inout Base, newValue: Double) { + base = Base(newValue) + } + + package init() { + _openSwiftUIEmptyStub() + } } - struct ToDouble: Projection { - func get(base: Value) -> Double { Double(base) } - func set(base: inout Value, newValue: Double) { base = Value(newValue) } + package struct ToDoubleFromInteger: Projection where Base: BinaryInteger { + package func get(base: Base) -> Double { + Double(base) + } + + package func set(base: inout Base, newValue: Double) { + base = Base(newValue) + } + + package init() { + _openSwiftUIEmptyStub() + } } - struct ToOptional: Projection { - func get(base: Value) -> Value? { base } + fileprivate struct Equals: Projection where Value: Hashable { + var value: Value - func set(base: inout Value, newValue: Value?) { - guard let newValue else { + func get(base: Value) -> Bool { + base == value + } + + func set(base: inout Value, newValue: Bool) { + guard newValue else { return } - base = newValue + base = value } } } From b49d658927437257497284ff940ad953a151a1dc Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 5 Oct 2025 15:25:23 +0800 Subject: [PATCH 8/8] Add BindingOperationsTests --- .../Data/Binding/BindingOperations.swift | 4 + .../MutableCollection+Extension.swift | 2 +- .../Data/Binding/BindingOperationsTests.swift | 463 +++++++++++++++++- .../DynamicPropertyBufferTests.swift | 2 +- .../Util/ConcatenatedCollectionTests.swift | 2 +- .../Data/Util/InlineArrayTests.swift | 2 +- .../MutableCollectionExtensionTests.swift | 4 +- 7 files changed, 471 insertions(+), 8 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift b/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift index 63f21a9e6..843597624 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift @@ -40,6 +40,10 @@ extension Binding { self = base.projecting(BindingOperations.ToDouble()) } + package init(_ base: Binding) where Value == Double, V: BinaryInteger { + self = base.projecting(BindingOperations.ToDoubleFromInteger()) + } + package static func == (lhs: Binding, rhs: Value) -> Binding where Value: Hashable { lhs.projecting(BindingOperations.Equals(value: rhs)) } diff --git a/Sources/OpenSwiftUICore/Util/Extension/MutableCollection+Extension.swift b/Sources/OpenSwiftUICore/Util/Extension/MutableCollection+Extension.swift index 7b91aaf4b..4f9dbc669 100644 --- a/Sources/OpenSwiftUICore/Util/Extension/MutableCollection+Extension.swift +++ b/Sources/OpenSwiftUICore/Util/Extension/MutableCollection+Extension.swift @@ -3,7 +3,7 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: Implmeneted by Copilot +// Author: Implmeneted by Copilot public import Foundation diff --git a/Tests/OpenSwiftUICoreTests/Data/Binding/BindingOperationsTests.swift b/Tests/OpenSwiftUICoreTests/Data/Binding/BindingOperationsTests.swift index c0db8adf3..bc6c9c85f 100644 --- a/Tests/OpenSwiftUICoreTests/Data/Binding/BindingOperationsTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/Binding/BindingOperationsTests.swift @@ -1,11 +1,468 @@ // // BindingOperationsTests.swift // OpenSwiftUITests +// +// Author: Claude Code with Claude Sonnet 4.5 -@testable import OpenSwiftUICore +import Foundation import Testing +@testable import OpenSwiftUICore struct BindingOperationsTests { - @Test - func forceUnwrapping() {} + + // MARK: - Binding.init(_:) to Optional + + struct ToOptionalTests { + @Test + func get() { + var storage = 42 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let optionalBinding: Binding = Binding(baseBinding) + #expect(optionalBinding.wrappedValue == 42) + } + + @Test + func setWithNonNil() { + var storage = 10 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let optionalBinding: Binding = Binding(baseBinding) + optionalBinding.wrappedValue = 20 + #expect(optionalBinding.wrappedValue == 10) + + storage = 20 + #expect(optionalBinding.wrappedValue == 20) + } + + @Test + func setWithNil() { + var storage = 10 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let optionalBinding: Binding = Binding(baseBinding) + optionalBinding.wrappedValue = nil + #expect(optionalBinding.wrappedValue == 10) + #expect(storage == 10) + } + } + + // MARK: - Binding.init(_:) to AnyHashable + + struct ToAnyHashableTests { + @Test + func get() { + var storage = 42 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let anyHashableBinding: Binding = Binding(baseBinding) + #expect(anyHashableBinding.wrappedValue == AnyHashable(42)) + } + + @Test + func set() { + var storage = 10 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let anyHashableBinding: Binding = Binding(baseBinding) + anyHashableBinding.wrappedValue = AnyHashable(20) + #expect(anyHashableBinding.wrappedValue == AnyHashable(10)) + + storage = 20 + #expect(anyHashableBinding.wrappedValue == AnyHashable(20)) + } + + @Test + func withString() { + var storage = "hello" + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let anyHashableBinding: Binding = Binding(baseBinding) + #expect(anyHashableBinding.wrappedValue == AnyHashable("hello")) + + anyHashableBinding.wrappedValue = AnyHashable("world") + #expect(anyHashableBinding.wrappedValue == AnyHashable("hello")) + + storage = "world" + #expect(anyHashableBinding.wrappedValue == AnyHashable("world")) + } + } + + // MARK: - Binding.init?(_:) ForceUnwrapping + + struct ForceUnwrappingTests { + @Test + func getNonNil() { + var storage: Int? = 42 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let unwrappedBinding = Binding(baseBinding) + #expect(unwrappedBinding != nil) + #expect(unwrappedBinding?.wrappedValue == 42) + } + + @Test + func getNil() { + var storage: Int? = nil + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let unwrappedBinding = Binding(baseBinding) + #expect(unwrappedBinding == nil) + } + + @Test + func set() { + var storage: Int? = 10 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + guard let unwrappedBinding = Binding(baseBinding) else { + #expect(Bool(false)) + return + } + + unwrappedBinding.wrappedValue = 20 + #expect(unwrappedBinding.wrappedValue == 10) + + storage = 20 + #expect(unwrappedBinding.wrappedValue == 20) + } + } + + // MARK: - Binding.subscript(keyPath:default:) (tests NilCoalescing) + + struct NilCoalescingTests { + @Test + func getWithValue() { + struct Model { + var value: Int? + } + + var storage = Model(value: 42) + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let nilCoalescingBinding = baseBinding[\.value, default: 100] + #expect(nilCoalescingBinding.wrappedValue == 42) + } + + @Test + func getWithNil() { + struct Model { + var value: Int? + } + + var storage = Model(value: nil) + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let nilCoalescingBinding = baseBinding[\.value, default: 100] + #expect(nilCoalescingBinding.wrappedValue == 100) + } + + @Test + func set() { + struct Model { + var value: Int? + } + + var storage = Model(value: nil) + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let nilCoalescingBinding = baseBinding[\.value, default: 100] + #expect(nilCoalescingBinding.wrappedValue == 100) + + nilCoalescingBinding.wrappedValue = 50 + #expect(nilCoalescingBinding.wrappedValue == 100) + + storage.value = 50 + #expect(nilCoalescingBinding.wrappedValue == 50) + } + + @Test + func setBackToNil() { + struct Model { + var value: Int? + } + + var storage = Model(value: 42) + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let nilCoalescingBinding = baseBinding[\.value, default: 100] + #expect(nilCoalescingBinding.wrappedValue == 42) + + storage.value = nil + #expect(nilCoalescingBinding.wrappedValue == 100) + } + } + + // MARK: - Binding.init(_:) to Double from BinaryFloatingPoint + + struct ToDoubleTests { + @Test + func getFromFloat() { + var storage: Float = 3.14 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let doubleBinding: Binding = Binding(baseBinding) + #expect(doubleBinding.wrappedValue.isApproximatelyEqual(to: 3.14, absoluteTolerance: 0.001)) + } + + @Test + func setFromFloat() { + var storage: Float = 1.0 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let doubleBinding: Binding = Binding(baseBinding) + doubleBinding.wrappedValue = 2.5 + #expect(doubleBinding.wrappedValue.isApproximatelyEqual(to: 1.0, absoluteTolerance: 0.001)) + + storage = 2.5 + #expect(doubleBinding.wrappedValue.isApproximatelyEqual(to: 2.5, absoluteTolerance: 0.001)) + } + + @Test + func getFromCGFloat() { + var storage: CGFloat = 3.14159 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let doubleBinding: Binding = Binding(baseBinding) + #expect(doubleBinding.wrappedValue.isApproximatelyEqual(to: 3.14159)) + } + + @Test + func setFromCGFloat() { + var storage: CGFloat = 1.0 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let doubleBinding: Binding = Binding(baseBinding) + doubleBinding.wrappedValue = 2.71828 + #expect(doubleBinding.wrappedValue.isApproximatelyEqual(to: 1.0)) + + storage = 2.71828 + #expect(doubleBinding.wrappedValue.isApproximatelyEqual(to: 2.71828)) + } + } + + // MARK: - Binding.init(_:) to Double from BinaryInteger + + struct ToDoubleFromIntegerTests { + @Test + func get() { + var storage = 42 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let doubleBinding: Binding = Binding(baseBinding) + #expect(doubleBinding.wrappedValue == 42.0) + } + + @Test + func set() { + var storage = 10 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let doubleBinding: Binding = Binding(baseBinding) + doubleBinding.wrappedValue = 25.7 + #expect(doubleBinding.wrappedValue == 10.0) + + storage = 25 + #expect(doubleBinding.wrappedValue == 25.0) + } + + @Test + func withUInt() { + var storage: UInt = 100 + let baseBinding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let doubleBinding: Binding = Binding(baseBinding) + #expect(doubleBinding.wrappedValue == 100.0) + + doubleBinding.wrappedValue = 200.5 + #expect(doubleBinding.wrappedValue == 100.0) + + storage = 200 + #expect(doubleBinding.wrappedValue == 200.0) + } + } + + // MARK: - Binding.== operator (tests Equals projection) + + struct EqualsTests { + @Test + func getWhenEqual() { + var storage = 42 + let binding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let resultBinding = binding == 42 + #expect(resultBinding.wrappedValue == true) + } + + @Test + func getWhenNotEqual() { + var storage = 10 + let binding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let resultBinding = binding == 42 + #expect(resultBinding.wrappedValue == false) + } + + @Test + func setWhenTrue() { + var storage = 10 + let binding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let resultBinding = binding == 42 + #expect(resultBinding.wrappedValue == false) + + resultBinding.wrappedValue = true + #expect(resultBinding.wrappedValue == false) + + storage = 42 + #expect(resultBinding.wrappedValue == true) + } + + @Test + func setWhenFalse() { + var storage = 42 + let binding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let resultBinding = binding == 42 + #expect(resultBinding.wrappedValue == true) + + resultBinding.wrappedValue = false + #expect(resultBinding.wrappedValue == true) + #expect(storage == 42) + } + + @Test + func withString() { + var storage = "hello" + let binding = Binding { + storage + } set: { newValue in + storage = newValue + } + + let resultBinding1 = binding == "hello" + #expect(resultBinding1.wrappedValue == true) + + let resultBinding2 = binding == "world" + #expect(resultBinding2.wrappedValue == false) + + resultBinding2.wrappedValue = true + #expect(resultBinding2.wrappedValue == false) + + storage = "world" + #expect(resultBinding2.wrappedValue == true) + + resultBinding2.wrappedValue = false + #expect(resultBinding2.wrappedValue == true) + } + } + + // MARK: - Binding.false + + struct BindingFalseTests { + @Test + func constantFalse() { + let falseBinding = Binding.false + #expect(falseBinding.wrappedValue == false) + + falseBinding.wrappedValue = true + #expect(falseBinding.wrappedValue == false) + } + } } diff --git a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyBufferTests.swift b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyBufferTests.swift index af5b926ca..b48210e47 100644 --- a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyBufferTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/DynamicPropertyBufferTests.swift @@ -2,7 +2,7 @@ // DynamicPropertyBufferTests.swift // OpenSwiftUICoreTests // -// Status: Created by GitHub Copilot with Claude Sonnet 4.5 +// Author: GitHub Copilot with Claude Sonnet 4.5 @_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore @testable import OpenSwiftUICore diff --git a/Tests/OpenSwiftUICoreTests/Data/Util/ConcatenatedCollectionTests.swift b/Tests/OpenSwiftUICoreTests/Data/Util/ConcatenatedCollectionTests.swift index 86d15164d..c0a7fe022 100644 --- a/Tests/OpenSwiftUICoreTests/Data/Util/ConcatenatedCollectionTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/Util/ConcatenatedCollectionTests.swift @@ -2,7 +2,7 @@ // ConcatenatedCollectionTests.swift // OpenSwiftUICoreTests // -// Status: Created by GitHub Copilot +// Author: GitHub Copilot import Testing @testable import OpenSwiftUICore diff --git a/Tests/OpenSwiftUICoreTests/Data/Util/InlineArrayTests.swift b/Tests/OpenSwiftUICoreTests/Data/Util/InlineArrayTests.swift index e8e234b9e..b0e1ba077 100644 --- a/Tests/OpenSwiftUICoreTests/Data/Util/InlineArrayTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/Util/InlineArrayTests.swift @@ -2,7 +2,7 @@ // InlineArrayTests.swift // OpenSwiftUICoreTests // -// Status: Created by GitHub Copilot +// Author: GitHub Copilot with Claude Sonnet 4.5 @testable import OpenSwiftUICore import Testing diff --git a/Tests/OpenSwiftUICoreTests/Util/Extension/MutableCollectionExtensionTests.swift b/Tests/OpenSwiftUICoreTests/Util/Extension/MutableCollectionExtensionTests.swift index f245d6a47..5a102e043 100644 --- a/Tests/OpenSwiftUICoreTests/Util/Extension/MutableCollectionExtensionTests.swift +++ b/Tests/OpenSwiftUICoreTests/Util/Extension/MutableCollectionExtensionTests.swift @@ -1,12 +1,14 @@ // // MutableCollectionExtensionTests.swift // OpenSwiftUICoreTests +// +// Author: Copilot import Foundation import OpenSwiftUICore import Testing -// MARK: - MutableCollectionExtensionTests [Implmeneted by Copilot] +// MARK: - MutableCollectionExtensionTests struct MutableCollectionExtensionTests { // MARK: - remove(atOffsets:)