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/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/Binding/Binding.swift b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift index f98d32755..337d2b6ed 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/Binding.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/Binding.swift @@ -2,11 +2,15 @@ // Binding.swift // OpenSwiftUICore // -// Audited for 3.5.2 +// Audited for 6.5.4 // 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. /// @@ -52,14 +56,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,17 +82,104 @@ public struct Binding { package var location: AnyLocation - var _value: Value + 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 { + { + 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 + /// 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. - 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) @@ -87,6 +188,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. @@ -94,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. /// @@ -174,7 +409,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. @@ -183,53 +418,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 @@ -262,6 +482,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) @@ -272,8 +493,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. /// @@ -299,82 +524,119 @@ 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 } - + func get() -> Value { base.get() } - + func set(_ value: Value, transaction: Transaction) { base.set(value, transaction: transaction) } - + 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 +// NOTE: This is currently not used +struct EnableRuntimeConcurrencyCheck: UserDefaultKeyedFeature { + static var key: String { "org.OpenSwiftUIProject.OpenSwiftUI.EnableRuntimeConcurrencyCheck" } -extension Binding { - package init(value: Value, location: AnyLocation, transaction: Transaction = Transaction()) { - self.transaction = transaction - self.location = location - self._value = value - } + static var cachedValue: Bool? - private func readValue() -> Value { - if GraphHost.isUpdating { - location.wasRead = true - return _value - } else { - return location.get() - } - } - - package func projecting(_ p: P) -> Binding where P.Base == Value { - Binding(value: p.get(base: _value), location: location.projecting(p), transaction: transaction) - } + static var isEnabled: Bool { true } } diff --git a/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift b/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift index cd4a7d9b2..843597624 100644 --- a/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift +++ b/Sources/OpenSwiftUICore/Data/Binding/BindingOperations.swift @@ -1,52 +1,172 @@ // // 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 { -extension BindingOperations { - struct ForceUnwrapping: Projection { - func get(base: Value?) -> Value { base! } - func set(base: inout Value?, newValue: Value) { base = newValue } + /// 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()) } - struct NilCoalescing: Projection { + /// 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 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)) + } +} + +private let _constantFalse: Binding = .constant(false) + +extension Binding where Value == Bool { + package static var `false`: Binding { + _constantFalse + } +} + +private var nilCoalescingGenerationCounter: Int = 0 + +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 + } + } + + // 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 - func get(base: Value?) -> Value { base ?? defaultValue } - func set(base: inout Value?, newValue: Value) { base = newValue } + package init(defaultValue: Value) { + self.defaultValue = defaultValue + self.generation = nilCoalescingGenerationCounter + nilCoalescingGenerationCounter += 1 + } + + package func get(base: Value?) -> Value { + base ?? defaultValue + } + + package func set(base: inout Value?, newValue: Value) { + base = newValue + } - static func == (lhs: BindingOperations.NilCoalescing, rhs: BindingOperations.NilCoalescing) -> Bool { + 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 } } } diff --git a/Sources/OpenSwiftUICore/Data/Binding/Location.swift b/Sources/OpenSwiftUICore/Data/Binding/Location.swift new file mode 100644 index 000000000..c204ef44d --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Binding/Location.swift @@ -0,0 +1,403 @@ +// +// Location.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 3C10A6E9BB0D4644A364890A9BD57D68 (SwiftUICore) + +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) +} + +extension Location { + package func update() -> (Value, Bool) { + (get(), true) + } +} + +// MARK: - AnyLocationBase + +/// The base type of all type-erased locations. +@available(OpenSwiftUI_v1_0, *) +@_documentation(visibility: private) +open class AnyLocationBase { + init() { + _openSwiftUIEmptyStub() + } +} + +@available(*, unavailable) +extension AnyLocationBase: Sendable {} + +// MARK: - AnyLocation + +/// The base type of all type-erased locations with value-type Value. +/// It is annotated as `@unchecked Sendable` so that user types such as +/// `State`, and `SceneStorage` can be cleanly `Sendable`. However, it is +/// 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) + open var wasRead: Bool { + get { _openSwiftUIBaseClassAbstractMethod() } + set { _openSwiftUIBaseClassAbstractMethod() } + } + + @_spi(ForOpenSwiftUIOnly) + open func get() -> Value { + _openSwiftUIBaseClassAbstractMethod() + } + + @_spi(ForOpenSwiftUIOnly) + open func projecting

(_ projection: P) -> AnyLocation where Value == P.Base, P: Projection { + _openSwiftUIBaseClassAbstractMethod() + } + + @_spi(ForOpenSwiftUIOnly) + open func set(_ newValue: Value, transaction: Transaction) { + _openSwiftUIBaseClassAbstractMethod() + } + + @_spi(ForOpenSwiftUIOnly) + open func update() -> (Value, Bool) { + _openSwiftUIBaseClassAbstractMethod() + } + + @_spi(ForOpenSwiftUIOnly) + open func isEqual(to other: AnyLocation) -> Bool { + self === other + } + + package override init() { + super.init() + } +} + +@available(OpenSwiftUI_v5_0, *) +extension AnyLocation: Equatable { + public static func == (lhs: AnyLocation, rhs: AnyLocation) -> Bool { + lhs.isEqual(to: rhs) + } +} + +@available(*, unavailable) +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 + + @AtomicBox + private var cache = LocationProjectionCache() + + package init(_ location: L) { + self.location = location + } + + override final package var wasRead: Bool { + get { location.wasRead } + set { location.wasRead = newValue } + } + + override final package func get() -> L.Value { + location.get() + } + + override final package func set(_ value: L.Value, transaction: Transaction) { + location.set(value, transaction: transaction) + } + + override final package func projecting

(_ projection: P) -> AnyLocation where P: Projection, L.Value == P.Base { + cache.reference(for: projection, on: location) + } + + override final package func update() -> (L.Value, Bool) { + location.update() + } + + override final package func isEqual(to other: AnyLocation) -> Bool { + if _SemanticFeature_v5.isEnabled { + if let otherBox = other as? LocationBox { + location == otherBox.location + } else { + false + } + } else { + other === self + } + } + + package typealias Value = L.Value +} + +// 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, + let result = base as? AnyLocation { + return result + } else { + let projectedLocation = ProjectedLocation(location: location, projection: projection) + let box = LocationBox(projectedLocation) + cache[projection as AnyHashable] = WeakBox(box) + return box + } + } + + /// Clears all cached projected locations. + package mutating func reset() { + cache = [:] + } + + package init() { + cache = [:] + } +} + +// 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 + } + + 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 + +/// 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 + } + + 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 + +/// 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 + } + + 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 + +/// 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) + } + + 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) { + self.location = location + self.projection = projection + } + + typealias Value = P.Projected + + var wasRead: Bool { + get { location.wasRead } + set { location.wasRead = newValue } + } + + func get() -> Value { + projection.get(base: location.get()) + } + + func set(_ value: Value, transaction _: Transaction) { + var base = location.get() + projection.set(base: &base, newValue: value) + } + + func update() -> (Value, Bool) { + let (base, result) = location.update() + let value = projection.get(base: base) + return (value, result) + } +} 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/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 deleted file mode 100644 index 972c0218d..000000000 --- a/Sources/OpenSwiftUICore/Data/Location/Location.swift +++ /dev/null @@ -1,224 +0,0 @@ -// -// Location.swift -// OpenSwiftUICore -// -// Audited for 6.0.87 -// Status: Complete -// ID: 3C10A6E9BB0D4644A364890A9BD57D68 - -// MARK: - Location - -package protocol Location: Equatable { - associatedtype Value - var wasRead: Bool { get set } - func get() -> Value - func set(_ value: Value, transaction: Transaction) - func update() -> (Value, Bool) -} - -extension Location { - package func update() -> (Value, Bool) { - (get(), true) - } -} - -// MARK: - AnyLocationBase - -/// The base type of all type-erased locations. -@_documentation(visibility: private) -open class AnyLocationBase {} - -@available(*, unavailable) -extension AnyLocationBase: Sendable {} - -// MARK: - AnyLocation - -/// The base type of all type-erased locations with value-type Value. -/// It is annotated as `@unchecked Sendable` so that user types such as -/// `State`, and `SceneStorage` can be cleanly `Sendable`. However, it is -/// 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). -@_documentation(visibility: private) -open class AnyLocation: AnyLocationBase, @unchecked Sendable { - @_spi(ForOpenSwiftUIOnly) - open var wasRead: Bool { - get { _openSwiftUIBaseClassAbstractMethod() } - set { _openSwiftUIBaseClassAbstractMethod() } - } - - @_spi(ForOpenSwiftUIOnly) - open func get() -> Value { - _openSwiftUIBaseClassAbstractMethod() - } - - @_spi(ForOpenSwiftUIOnly) - open func projecting

(_ projection: P) -> AnyLocation where Value == P.Base, P: Projection { - _openSwiftUIBaseClassAbstractMethod() - } - - @_spi(ForOpenSwiftUIOnly) - open func set(_ value: Value, transaction: Transaction) { - _openSwiftUIBaseClassAbstractMethod() - } - - @_spi(ForOpenSwiftUIOnly) - open func update() -> (Value, Bool) { - _openSwiftUIBaseClassAbstractMethod() - } - - @_spi(ForOpenSwiftUIOnly) - open func isEqual(to other: AnyLocation) -> Bool { - self === other - } - - package override init() { - super.init() - } -} - -extension AnyLocation: Equatable { - public static func == (lhs: AnyLocation, rhs: AnyLocation) -> Bool { - lhs.isEqual(to: rhs) - } -} - -@available(*, unavailable) -extension AnyLocation: Sendable {} - -// MARK: - LocationBox - -final package class LocationBox: AnyLocation, Location, @unchecked Sendable where L: Location { - final private(set) package var location: L - - @AtomicBox - private var cache = LocationProjectionCache() - - package init(_ location: L) { - self.location = location - } - - override final package var wasRead: Bool { - get { location.wasRead } - set { location.wasRead = newValue } - } - - override final package func get() -> L.Value { - location.get() - } - - override final package func set(_ value: L.Value, transaction: Transaction) { - location.set(value, transaction: transaction) - } - - override final package func projecting

(_ projection: P) -> AnyLocation where P: Projection, L.Value == P.Base { - cache.reference(for: projection, on: location) - } - - override final package func update() -> (L.Value, Bool) { - location.update() - } - - override final package func isEqual(to other: AnyLocation) -> Bool { - if _SemanticFeature_v5.isEnabled { - if let otherBox = other as? LocationBox { - location == otherBox.location - } else { - false - } - } else { - other === self - } - } - - package typealias Value = L.Value -} - -// MARK: - LocationProjectionCache - -package struct LocationProjectionCache { - var cache: [AnyHashable: WeakBox] - - 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, - let result = base as? AnyLocation { - return result - } else { - let projectedLocation = ProjectedLocation(location: location, projection: projection) - let box = LocationBox(projectedLocation) - cache[projection as AnyHashable] = WeakBox(box) - return box - } - } - package mutating func reset() { - cache = [:] - } - - package init() { - cache = [:] - } -} - -private struct ProjectedLocation: Location where P.Base == L.Value { - var location: L - var projection: P - - init(location: L, projection: P) { - self.location = location - self.projection = projection - } - - typealias Value = P.Projected - - var wasRead: Bool { - get { location.wasRead } - set { location.wasRead = newValue } - } - - func get() -> Value { projection.get(base: location.get()) } - - func set(_ value: Value, transaction _: Transaction) { - var base = location.get() - projection.set(base: &base, newValue: value) - } - - func update() -> (Value, Bool) { - let (base, result) = location.update() - let value = projection.get(base: base) - 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/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/Location/LocationTests.swift b/Tests/OpenSwiftUICoreTests/Data/Binding/LocationTests.swift similarity index 79% rename from Tests/OpenSwiftUICoreTests/Data/Location/LocationTests.swift rename to Tests/OpenSwiftUICoreTests/Data/Binding/LocationTests.swift index 74a3b348b..3462477a5 100644 --- a/Tests/OpenSwiftUICoreTests/Data/Location/LocationTests.swift +++ b/Tests/OpenSwiftUICoreTests/Data/Binding/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) + } } 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/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/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:)