diff --git a/Sources/OpenSwiftUICore/View/Trait/ViewTraitCollection.swift b/Sources/OpenSwiftUICore/View/Trait/ViewTraitCollection.swift deleted file mode 100644 index 0c427b942..000000000 --- a/Sources/OpenSwiftUICore/View/Trait/ViewTraitCollection.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// ViewTraitCollection.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: WIP -// ID: 9929B476764059557433A108298EE66F - -package struct ViewTraitCollection { - package init() { - self.storage = [] - } - - private var storage: [any AnyViewTrait] - - private struct AnyTrait: AnyViewTrait { - var value: Key.Value - - init(value: Key.Value) { - self.value = value - } - - var id: ObjectIdentifier { ObjectIdentifier(Key.self) } - - subscript() -> V { - get { value as! V } - set { value = newValue as! Key.Value } - } - } - - subscript(_: Key.Type) -> Key.Value { - get { - value(for: Key.self) - } - set { - if let index = storage.firstIndex(where: { $0.id == ObjectIdentifier(Key.self) }) { - storage[index][] = newValue - } else { - storage.append(AnyTrait(value: newValue)) - } - } - } - - func value(for _: Key.Type, defaultValue: Key.Value = Key.defaultValue) -> Key.Value { - storage.first { $0.id == ObjectIdentifier(Key.self) }?[] ?? defaultValue - } - - mutating func setValueIfUnset(_ value: Key.Value, for _: Key.Type) { - guard !storage.contains(where: { $0.id == ObjectIdentifier(Key.self) }) else { - return - } - storage.append(AnyTrait(value: value)) - } - -// func insertInteraction(for: OnInsertInteraction.Strategy) -> OnInsertInteraction? { -// preconditionFailure("TODO") -// } -// -// var optionalTransition: AnyTransition? { -// preconditionFailure("TODO") -// } -} - -private protocol AnyViewTrait { - var id: ObjectIdentifier { get } - subscript() -> V { get set } -} diff --git a/Sources/OpenSwiftUICore/View/Trait/ViewTraitKey.swift b/Sources/OpenSwiftUICore/View/Trait/ViewTraitKey.swift deleted file mode 100644 index 404bf6a46..000000000 --- a/Sources/OpenSwiftUICore/View/Trait/ViewTraitKey.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// ViewTraitKey.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Complete - -public protocol _ViewTraitKey { - associatedtype Value - static var defaultValue: Value { get } -} diff --git a/Sources/OpenSwiftUICore/View/Trait/ViewTraitKeys.swift b/Sources/OpenSwiftUICore/View/Trait/ViewTraitKeys.swift deleted file mode 100644 index 4b0da3483..000000000 --- a/Sources/OpenSwiftUICore/View/Trait/ViewTraitKeys.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ViewTraitKeys.swift -// OpenSwiftUI -// -// Audited for iOS 18.0 -// Status: Complete - -package struct ViewTraitKeys { - package var types: Set - package var isDataDependent: Bool - - package init() { - types = [] - isDataDependent = false - } - - package func contains(_ type: T.Type) -> Bool where T: _ViewTraitKey{ - types.contains(ObjectIdentifier(type)) - } - - package mutating func insert(_ type: T.Type) where T: _ViewTraitKey { - types.insert(ObjectIdentifier(type)) - } - - package mutating func formUnion(_ other: ViewTraitKeys) { - types.formUnion(other.types) - isDataDependent = isDataDependent || other.isDataDependent - } - - package func withDataDependent() -> ViewTraitKeys { - var copy = self - copy.isDataDependent = true - return copy - } -} diff --git a/Sources/OpenSwiftUICore/View/Variadic/ViewTrait.swift b/Sources/OpenSwiftUICore/View/Variadic/ViewTrait.swift new file mode 100644 index 000000000..ed1eee232 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Variadic/ViewTrait.swift @@ -0,0 +1,239 @@ +// +// ViewTrait.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: Blocked by ViewList +// ID: 9929B476764059557433A108298EE66F (SwiftUI) +// ID: 48526BA25CDCBF890FA91D018A5421B4 (SwiftUICore) + +import OpenGraphShims + +// MARK: - ViewTraitKey + +/// A type of key for a trait associated with the content of a +/// container view. +public protocol _ViewTraitKey { + /// The type of value produced by the trait. + associatedtype Value + + /// The default value of the trait. + static var defaultValue: Value { get } +} + +// MARK: - _TraitWritingModifier [TODO] + +/// A view content adapter that associates a trait with its base content. +@frozen +public struct _TraitWritingModifier: PrimitiveViewModifier where Trait : _ViewTraitKey { + public let value: Trait.Value + + @inlinable + public init(value: Trait.Value) { + self.value = value + } + + nonisolated public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + preconditionFailure("TODO") + } + nonisolated public static func _makeViewList( + modifier: _GraphValue, + inputs: _ViewListInputs, + body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs + ) -> _ViewListOutputs { + preconditionFailure("TODO") + } + + nonisolated public static func _viewListCount(inputs: _ViewListCountInputs, body: (_ViewListCountInputs) -> Int?) -> Int? { + preconditionFailure("TODO") + } + + private struct AddTrait { + @Attribute var modifier: _TraitWritingModifier + @OptionalAttribute var traits: ViewTraitCollection? + } +} + +@available(*, unavailable) +extension _TraitWritingModifier: Sendable {} + +extension View { + /// Associate a trait `value` for the given `key` for this view content. + @inlinable + nonisolated public func _trait(_ key: K.Type, _ value: K.Value) -> some View where K: _ViewTraitKey { + return modifier(_TraitWritingModifier(value: value)) + } +} + +// MARK: - _ConditionalTraitWritingModifier [TODO] + +/// Conditionally writes a trait. +@frozen +public struct _ConditionalTraitWritingModifier: PrimitiveViewModifier where Trait : _ViewTraitKey { + public var value: Trait.Value + public var isEnabled: Bool + + @_alwaysEmitIntoClient + public init(traitKey: Trait.Type = Trait.self, value: Trait.Value, isEnabled: Bool) { + self.value = value + self.isEnabled = isEnabled + } + + nonisolated public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + preconditionFailure("TODO") + } + nonisolated public static func _makeViewList( + modifier: _GraphValue, + inputs: _ViewListInputs, + body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs + ) -> _ViewListOutputs { + preconditionFailure("TODO") + } + + nonisolated public static func _viewListCount(inputs: _ViewListCountInputs, body: (_ViewListCountInputs) -> Int?) -> Int? { + preconditionFailure("TODO") + } +} + +@available(*, unavailable) +extension _ConditionalTraitWritingModifier: Sendable {} + +extension View { + @_alwaysEmitIntoClient + public func _trait(_ key: K.Type = K.self, _ value: K.Value, isEnabled: Bool) -> some View where K: _ViewTraitKey { + modifier(_ConditionalTraitWritingModifier( + value: value, + isEnabled: isEnabled + )) + } +} + +// MARK: - TraitTransformerModifier [TODO] + +struct TraitTransformerModifier { + +} + +extension View { + package func transformTrait(_ key: K.Type = K.self, transform: @escaping (inout K.Value) -> Void) -> some View where K: _ViewTraitKey { + preconditionFailure("TODO") + } +} + +// MARK: - ViewTraitCollection + +private protocol AnyViewTrait { + var id: ObjectIdentifier { get } + subscript() -> V { get set } +} + +package struct ViewTraitCollection { + package init() { + self.storage = [] + } + + package func contains(_ key: Trait.Type) -> Bool where Trait: _ViewTraitKey { + storage.contains { $0.id == ObjectIdentifier(key) } + } + + package func value(for key: Trait.Type, defaultValue: Trait.Value) -> Trait.Value where Trait: _ViewTraitKey { + storage.first { $0.id == ObjectIdentifier(key) }?[] ?? defaultValue + } + + package func value(for key: Trait.Type) -> Trait.Value where Trait: _ViewTraitKey { + value(for: key, defaultValue: key.defaultValue) + } + + package mutating func setValueIfUnset(_ value: Trait.Value, for key: Trait.Type) where Trait: _ViewTraitKey { + guard !storage.contains(where: { $0.id == ObjectIdentifier(key) }) else { + return + } + storage.append(AnyTrait(value: value)) + } + + package subscript(key: Trait.Type) -> Trait.Value where Trait : _ViewTraitKey { + get { + value(for: key) + } + set { + if let index = storage.firstIndex(where: { $0.id == ObjectIdentifier(key) }) { + storage[index][] = newValue + } else { + storage.append(AnyTrait(value: newValue)) + } + } + } + + package mutating func mergeValues(_ traits: ViewTraitCollection) { + for trait in traits.storage { + setErasedValue(trait: trait) + } + } + + private mutating func setErasedValue(trait: ViewTrait) where ViewTrait: AnyViewTrait { + if let index = storage.firstIndex(where: { $0.id == trait.id }) { + let value: Any = trait[] + storage[index][] = value + } else { + storage.append(trait) + } + } + + private var storage: [any AnyViewTrait] + + private struct AnyTrait: AnyViewTrait where Trait: _ViewTraitKey { + typealias Value = Trait.Value + + var value: Value + + init(value: Trait.Value) { + self.value = value + } + + var id: ObjectIdentifier { ObjectIdentifier(Trait.self) } + + subscript() -> V { + get { value as! V } + set { value = newValue as! Value } + } + } +} + +// MARK: - ViewTraitKeys + +package struct ViewTraitKeys { + package var types: Set + package var isDataDependent: Bool + + package init() { + types = [] + isDataDependent = false + } + + package func contains(_ type: T.Type) -> Bool where T: _ViewTraitKey{ + types.contains(ObjectIdentifier(type)) + } + + package mutating func insert(_ type: T.Type) where T: _ViewTraitKey { + types.insert(ObjectIdentifier(type)) + } + + package mutating func formUnion(_ other: ViewTraitKeys) { + types.formUnion(other.types) + isDataDependent = isDataDependent || other.isDataDependent + } + + package func withDataDependent() -> ViewTraitKeys { + var copy = self + copy.isDataDependent = true + return copy + } +} diff --git a/Tests/OpenSwiftUICoreTests/View/AnyViewTests.swift b/Tests/OpenSwiftUICoreTests/View/AnyViewTests.swift index 697423d93..b1d289443 100644 --- a/Tests/OpenSwiftUICoreTests/View/AnyViewTests.swift +++ b/Tests/OpenSwiftUICoreTests/View/AnyViewTests.swift @@ -1,6 +1,6 @@ // // AnyViewTests.swift -// OpenSwiftUITests +// OpenSwiftUICoreTests @testable import OpenSwiftUICore import Testing diff --git a/Tests/OpenSwiftUICoreTests/View/Variadic/ViewTraitTests.swift b/Tests/OpenSwiftUICoreTests/View/Variadic/ViewTraitTests.swift new file mode 100644 index 000000000..d31bfcbdc --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/View/Variadic/ViewTraitTests.swift @@ -0,0 +1,138 @@ +// +// ViewTraitTests.swift +// OpenSwiftUICoreTests + +@testable import OpenSwiftUICore +import Testing +import Numerics + +fileprivate struct IntTrait: _ViewTraitKey { + static let defaultValue: Int = .zero +} + +fileprivate struct Int32Trait: _ViewTraitKey { + static let defaultValue: Int32 = .zero +} + +fileprivate struct DoubleTrait: _ViewTraitKey { + static let defaultValue: Double = .zero +} + +struct ViewTraitCollectionTests { + @Test("Test ViewTraitCollection contains API") + func contains() { + var collection = ViewTraitCollection() + #expect(collection.contains(IntTrait.self) == false) + #expect(collection.contains(DoubleTrait.self) == false) + + collection[IntTrait.self] = 1 + #expect(collection.contains(IntTrait.self) == true) + + collection[DoubleTrait.self] = 1.0 + #expect(collection.contains(DoubleTrait.self) == true) + } + + @Test("Test ViewTraitCollection value abd setValueIfUnset API") + func value() { + var collection = ViewTraitCollection() + + #expect(collection.value(for: IntTrait.self) == IntTrait.defaultValue) + #expect(collection.value(for: DoubleTrait.self).isApproximatelyEqual(to: DoubleTrait.defaultValue)) + + collection[IntTrait.self] = 1 + collection[DoubleTrait.self] = 1.0 + + #expect(collection.value(for: IntTrait.self) == 1) + #expect(collection.value(for: DoubleTrait.self).isApproximatelyEqual(to: 1.0)) + + collection.setValueIfUnset(2, for: IntTrait.self) + collection.setValueIfUnset(2.0, for: DoubleTrait.self) + + // Values should not change since they were already set + #expect(collection.value(for: IntTrait.self) == 1) + #expect(collection.value(for: DoubleTrait.self).isApproximatelyEqual(to: 1.0)) + + collection[IntTrait.self] = 3 + collection[DoubleTrait.self] = 3.0 + + #expect(collection.value(for: IntTrait.self) == 3) + #expect(collection.value(for: DoubleTrait.self).isApproximatelyEqual(to: 3.0)) + } + + @Test("Test ViewTraitCollection mergeValues API") + func mergeValues() { + var collection1 = ViewTraitCollection() + var collection2 = ViewTraitCollection() + + collection1[IntTrait.self] = 1 + collection1[DoubleTrait.self] = 1.0 + + collection2[Int32Trait.self] = 2 + collection2[DoubleTrait.self] = 2.0 + + collection1.mergeValues(collection2) + #expect(collection1.value(for: IntTrait.self) == 1) + #expect(collection1.value(for: Int32Trait.self) == 2) + #expect(collection1.value(for: DoubleTrait.self).isApproximatelyEqual(to: 2.0)) + + // Test merging with empty collection + let emptyCollection = ViewTraitCollection() + collection1.mergeValues(emptyCollection) + + // Values should remain unchanged + #expect(collection1.value(for: IntTrait.self) == 1) + #expect(collection1.value(for: Int32Trait.self) == 2) + #expect(collection1.value(for: DoubleTrait.self).isApproximatelyEqual(to: 2.0)) + } +} + +struct ViewTraitKeyTests { + @Test("Test ViewTraitKeys contains API") + func contains() { + var keys = ViewTraitKeys() + #expect(keys.contains(IntTrait.self) == false) + #expect(keys.contains(Int32Trait.self) == false) + #expect(keys.contains(DoubleTrait.self) == false) + + keys.insert(IntTrait.self) + #expect(keys.contains(IntTrait.self) == true) + #expect(keys.contains(Int32Trait.self) == false) + #expect(keys.contains(DoubleTrait.self) == false) + } + + @Test("Test ViewTraitKeys formUnion API") + func formUnion() { + var keys1 = ViewTraitKeys() + var keys2 = ViewTraitKeys() + + keys1.insert(IntTrait.self) + keys2.insert(Int32Trait.self) + keys2.insert(DoubleTrait.self) + + keys1.formUnion(keys2) + + #expect(keys1.contains(IntTrait.self) == true) + #expect(keys1.contains(Int32Trait.self) == true) + #expect(keys1.contains(DoubleTrait.self) == true) + #expect(keys1.isDataDependent == false) + + // Test union with data dependent keys + let dataDependent = keys2.withDataDependent() + keys1.formUnion(dataDependent) + #expect(keys1.isDataDependent == true) + } + + @Test("Test ViewTraitKeys withDataDependent API") + func withDataDependent() { + var keys = ViewTraitKeys() + keys.insert(IntTrait.self) + #expect(keys.isDataDependent == false) + + let dependent = keys.withDataDependent() + #expect(dependent.isDataDependent == true) + #expect(dependent.contains(IntTrait.self) == true) + + // Original should remain unchanged + #expect(keys.isDataDependent == false) + } +}