From fa9a2a7332464602b6ae94c8d044cf623d4decb6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 19 Jul 2025 14:40:36 +0800 Subject: [PATCH 1/2] Update UIKitEnvironment --- .../Data/Environment/BridgedEnvironment.swift | 16 --- .../Data/Environment/UIKitEnvironment.swift | 102 ++++++++++++++ .../Environment/UIKitEnvironmentKey.swift | 129 ++++++++++++++++++ .../UIKit/OpenSwiftUI+UITraitCollection.swift | 21 --- .../Data/Environment/EnvironmentKey.swift | 10 +- .../Data/Environment/EnvironmentValues.swift | 4 +- .../OpenSwiftUICore/Data/PropertyList.swift | 2 +- .../Shims/UIKit/UIKit_Private.h | 4 + .../Shims/UIKit/UIKit_Private.m | 6 + 9 files changed, 253 insertions(+), 41 deletions(-) delete mode 100644 Sources/OpenSwiftUI/Data/Environment/BridgedEnvironment.swift create mode 100644 Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift create mode 100644 Sources/OpenSwiftUI/Data/Environment/UIKitEnvironmentKey.swift delete mode 100644 Sources/OpenSwiftUI/Integration/Graphic/UIKit/OpenSwiftUI+UITraitCollection.swift diff --git a/Sources/OpenSwiftUI/Data/Environment/BridgedEnvironment.swift b/Sources/OpenSwiftUI/Data/Environment/BridgedEnvironment.swift deleted file mode 100644 index 3bfa19f48..000000000 --- a/Sources/OpenSwiftUI/Data/Environment/BridgedEnvironment.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// BridgedEnvironment.swift -// OpenSwiftUI -// -// Status: WIP -// ID: 005A2BB2D44F4D559B7E508DC5B95FF (SwiftUI?) - -#if os(iOS) - -import UIKit - -struct InheritedTraitCollectionKey: EnvironmentKey { - static var defaultValue: UITraitCollection? { nil } -} - -#endif diff --git a/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift new file mode 100644 index 000000000..c3347967b --- /dev/null +++ b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironment.swift @@ -0,0 +1,102 @@ +// +// UIKitEnvironment.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP +// ID: 005A2BB2D44F4D559B7E508DC5B95FFB (SwiftUI) + +#if canImport(UIKit) + +package import OpenSwiftUICore +package import UIKit + +private struct BridgedEnvironmentKeysKey: EnvironmentKey { + static let defaultValue: [any UITraitBridgedEnvironmentKey.Type] = [] +} + +extension EnvironmentValues { + @inline(__always) + var bridgedEnvironmentKeys: [any UITraitBridgedEnvironmentKey.Type] { + get { self[BridgedEnvironmentKeysKey.self] } + set { self[BridgedEnvironmentKeysKey.self] = newValue } + } +} + +extension UITraitCollection { + package func byOverriding(with environment: EnvironmentValues, viewPhase: ViewPhase, focusedValues: FocusedValues) -> UITraitCollection { + let wrapper = EnvironmentWrapper(environment: environment, phase: viewPhase, focusedValues: focusedValues) + return resolvedTraitCollection(with: environment, wrapper: wrapper) + } + + private func resolvedTraitCollection( + with environment: EnvironmentValues, + wrapper: EnvironmentWrapper?, + forImageAssetsOnly: Bool = false + ) -> UITraitCollection { + _modifyingTraits(environmentWrapper: wrapper) { mutableTraits in + func copyValueToMutableTraits(for key: K.Type) where K: UITraitBridgedEnvironmentKey { + K.write(to: &mutableTraits, value: environment[key]) + } + for bridgedKey in environment.bridgedEnvironmentKeys { + copyValueToMutableTraits(for: bridgedKey) + } + let layoutDirection = UITraitEnvironmentLayoutDirection(environment.layoutDirection) + if layoutDirection != mutableTraits.layoutDirection { + mutableTraits.layoutDirection = layoutDirection + } + let displayScale = environment.displayScale + if displayScale != mutableTraits.displayScale { + mutableTraits.displayScale = displayScale + } + // TODO + _openSwiftUIUnimplementedWarning() + } + } +} + +@objc(SwiftUIEnvironmentWrapper) +private final class EnvironmentWrapper: NSObject, NSSecureCoding { + let environment: EnvironmentValues + let phase: ViewPhase + let focusedValues: FocusedValues + + init(environment: EnvironmentValues, phase: ViewPhase, focusedValues: FocusedValues) { + self.environment = environment + self.phase = phase + self.focusedValues = focusedValues + super.init() + } + + init?(coder: NSCoder) { + return nil + } + + func encode(with coder: NSCoder) {} + + override func isEqual(_ object: Any?) -> Bool { + guard let object, + let otherWrapper = object as? EnvironmentWrapper else { + return false + } + return phase == otherWrapper.phase && + !environment.plist.mayNotBeEqual(to: otherWrapper.environment.plist) && + !focusedValues.plist.mayNotBeEqual(to: otherWrapper.focusedValues.plist) + } + + static var supportsSecureCoding: Bool { true } +} + +extension UITraitCollection { + @_silgen_name("$sSo17UITraitCollectionC5UIKitE16_modifyingTraits18environmentWrapper9mutationsABSo8NSObjectCSg_yAC09UIMutableE0_pzXEtF") + func _modifyingTraits( + environmentWrapper: NSObject?, + mutations: (inout UIMutableTraits) -> () + ) -> UITraitCollection +} + +struct InheritedTraitCollectionKey: EnvironmentKey { + static var defaultValue: UITraitCollection? { nil } +} + +#endif diff --git a/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironmentKey.swift b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironmentKey.swift new file mode 100644 index 000000000..f1cb29338 --- /dev/null +++ b/Sources/OpenSwiftUI/Data/Environment/UIKitEnvironmentKey.swift @@ -0,0 +1,129 @@ +// +// UIKitEnvironmentKey.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 859636D0EA4E0B7C4D7D1B41B613A4D5 (SwiftUI?) + +#if canImport(UIKit) + +public import OpenSwiftUICore +public import UIKit + +/// An environment key that is bridged to a UIKit trait. +/// +/// Use this protocol to allow the same underlying data to be accessed using an +/// environment key in OpenSwiftUI and trait in UIKit. As the bridging is +/// bidirectional, values written to the trait in UIKit can be read using the +/// environment key in OpenSwiftUI, and values written to the environment key in +/// OpenSwiftUI can be read from the trait in UIKit. +/// +/// Given a custom UIKit trait named `MyTrait` with `myTrait` properties on +/// both `UITraitCollection` and `UIMutableTraits`: +/// +/// struct MyTrait: UITraitDefinition { +/// static let defaultValue = "Default value" +/// } +/// +/// extension UITraitCollection { +/// var myTrait: String { +/// self[MyTrait.self] +/// } +/// } +/// +/// extension UIMutableTraits { +/// var myTrait: String { +/// get { self[MyTrait.self] } +/// set { self[MyTrait.self] = newValue } +/// } +/// } +/// +/// You can declare an environment key to represent the same data: +/// +/// struct MyEnvironmentKey: EnvironmentKey { +/// static let defaultValue = "Default value" +/// } +/// +/// Bridge the environment key and the trait by conforming to the +/// `UITraitBridgedEnvironmentKey` protocol, providing implementations +/// of ``read(from:)`` and ``write(to:value:)`` to losslessly convert +/// the environment value from and to the corresponding trait value: +/// +/// extension MyEnvironmentKey: UITraitBridgedEnvironmentKey { +/// static func read( +/// from traitCollection: UITraitCollection +/// ) -> String { +/// traitCollection.myTrait +/// } +/// +/// static func write( +/// to mutableTraits: inout UIMutableTraits, value: String +/// ) { +/// mutableTraits.myTrait = value +/// } +/// } +/// +@available(OpenSwiftUI_v5_0, *) +@available(macOS, unavailable) +@available(watchOS, unavailable) +public protocol UITraitBridgedEnvironmentKey: EnvironmentKey { + /// Reads the trait value from the trait collection, and returns + /// the equivalent environment value. + /// + /// - Parameter traitCollection: The trait collection to read from. + static func read(from traitCollection: UITraitCollection) -> Value + + /// Writes the equivalent trait value for the environment value into + /// the mutable traits. + /// + /// - Parameter mutableTraits: The mutable traits to write to. + /// - Parameter value: The environment value to write. + static func write(to mutableTraits: inout any UIMutableTraits, value: Value) +} + + +extension EnvironmentValues { + subscript(_ key: K.Type) -> K.Value where K: UITraitBridgedEnvironmentKey { + get { getBridgeValue(for: key) } + set { setBridgeValue(value: newValue, for: K.self) } + } + + private func getBridgeValue(for key: K.Type) -> K.Value where K: UITraitBridgedEnvironmentKey { + valueWithSecondaryLookup(UITraitBridgedEnvironmentPropertyKeyLookup.self) + } + + private mutating func setBridgeValue(value: K.Value, for key: K.Type) where K: UITraitBridgedEnvironmentKey { + if !bridgedEnvironmentKeys.contains(where: { $0 == key }) { + bridgedEnvironmentKeys.append(key) + } + setValue(value, for: key) + } +} + +private struct UITraitBridgedEnvironmentPropertyKeyLookup: PropertyKeyLookup where K: UITraitBridgedEnvironmentKey { + typealias Primary = EnvironmentPropertyKey + typealias Secondary = EnvironmentPropertyKey + + static func lookup(in traitCollection: UITraitCollection?) -> K.Value? { + traitCollection.map { K.read(from: $0) } + } +} + +struct UITraitBridgedEnvironmentResolver: BridgedEnvironmentResolver { + static func read(for key: K.Type, from environment: EnvironmentValues) -> K.Value where K: EnvironmentKey { + let bridgeKey = key as! any UITraitBridgedEnvironmentKey.Type + return environment[bridgeKey] as! K.Value + } + + static func write(for key: K.Type, to environment: inout EnvironmentValues, value: K.Value) where K: EnvironmentKey { + let bridgeKey = key as! any UITraitBridgedEnvironmentKey.Type + write(bridgedKey: bridgeKey, to: &environment, value: value) + } + + static func write(bridgedKey: K.Type, to environment: inout EnvironmentValues, value: V) where K: UITraitBridgedEnvironmentKey { + environment[K.self] = value as! K.Value + } +} + +#endif diff --git a/Sources/OpenSwiftUI/Integration/Graphic/UIKit/OpenSwiftUI+UITraitCollection.swift b/Sources/OpenSwiftUI/Integration/Graphic/UIKit/OpenSwiftUI+UITraitCollection.swift deleted file mode 100644 index 283a473e5..000000000 --- a/Sources/OpenSwiftUI/Integration/Graphic/UIKit/OpenSwiftUI+UITraitCollection.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// OpenSwiftUI+UITraitCollection.swift -// OpenSwiftUI -// -// Audited for iOS 18.0 -// Status: WIP -// ID: 005A2BB2D44F4D559B7E508DC5B95FFB (SwiftUI?) - -#if canImport(UIKit) - -import OpenSwiftUICore -import UIKit - -extension UITraitCollection { - func byOverriding(with: EnvironmentValues, viewPhase: _GraphInputs.Phase, focusedValues: FocusedValues) -> UITraitCollection { - // TODO - self - } -} - -#endif diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKey.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKey.swift index 1ea0332dc..290c94d22 100644 --- a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKey.swift +++ b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKey.swift @@ -2,7 +2,7 @@ // EnvironmentKey.swift // OpenSwiftUICore // -// Audited for iOS 18.0 +// Audited for 6.5.4 // Status: Complete import OpenGraphShims @@ -138,3 +138,11 @@ package protocol DerivedEnvironmentKey { /// - Returns: The derived value for this key. static func value(in: EnvironmentValues) -> Value } + +package protocol BridgedEnvironmentKey: EnvironmentKey {} + +package protocol BridgedEnvironmentResolver { + static func read(for key: K.Type, from environment: EnvironmentValues) -> K.Value where K: EnvironmentKey + + static func write(for key: K.Type, to environment: inout EnvironmentValues, value: K.Value) where K: EnvironmentKey +} diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentValues.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentValues.swift index 34cfddcda..4e057dd06 100644 --- a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentValues.swift +++ b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentValues.swift @@ -242,9 +242,9 @@ extension EnvironmentValues: Sendable {} /// A property key that provides access to environment values. /// /// This type bridges between the `EnvironmentKey` protocol and the internal `PropertyKey` system. -private struct EnvironmentPropertyKey: PropertyKey where Key: EnvironmentKey { +package struct EnvironmentPropertyKey: PropertyKey where Key: EnvironmentKey { /// The default value for this property key, obtained from the environment key. - static var defaultValue: Key.Value { + package static var defaultValue: Key.Value { Key.defaultValue } } diff --git a/Sources/OpenSwiftUICore/Data/PropertyList.swift b/Sources/OpenSwiftUICore/Data/PropertyList.swift index beffe41e2..99cfbb0b1 100644 --- a/Sources/OpenSwiftUICore/Data/PropertyList.swift +++ b/Sources/OpenSwiftUICore/Data/PropertyList.swift @@ -78,7 +78,7 @@ package protocol PropertyKeyLookup { /// /// - Parameter secondaryValue: The secondary value to use for lookup. /// - Returns: The primary value if found, or `nil` if not found. - static func lookup(in: Secondary.Value) -> Primary.Value? + static func lookup(in value: Secondary.Value) -> Primary.Value? } // MARK: - PropertyList diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h index d921c0ed2..1fb6acbff 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h +++ b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h @@ -43,6 +43,10 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) UIUserInterfaceStyle _systemUserInterfaceStyle_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(_systemUserInterfaceStyle); @end +@interface UITraitCollection (OpenSwiftUI_SPI) +@property (nonatomic, readonly, nullable) NSObject *_environmentWrapper_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(_environmentWrapper); +@end + OPENSWIFTUI_EXPORT bool UIViewIgnoresTouchEvents(UIView *view); diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m index 5d6300580..f4c559f6e 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m +++ b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m @@ -75,5 +75,11 @@ - (UIUserInterfaceStyle) _systemUserInterfaceStyle_openswiftui_safe_wrapper { } @end +@implementation UITraitCollection (OpenSwiftUI_SPI) +- (NSObject *)_environmentWrapper_openswiftui_safe_wrapper { + OPENSWIFTUI_SAFE_WRAPPER_IMP(NSObject *, @"_environmentWrapper", nil); + return func(self, selector); +} +@end #endif /* UIKit.h */ From 25c4cdfa9970dc284ed7a5cf63870d5de10779f7 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 19 Jul 2025 14:41:10 +0800 Subject: [PATCH 2/2] Add UIKitEnvironmentTests --- .../Event/Focus/FocusedValueKey.swift | 73 ++++++++++++++++--- .../EnvironmentValuesOpenURLTests.swift | 5 +- .../Environment/UIKitEnvironmentTests.swift | 21 ++++++ 3 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 Tests/OpenSwiftUITests/Data/Environment/UIKitEnvironmentTests.swift diff --git a/Sources/OpenSwiftUI/Event/Focus/FocusedValueKey.swift b/Sources/OpenSwiftUI/Event/Focus/FocusedValueKey.swift index aa68f4efb..d5a2e3c34 100644 --- a/Sources/OpenSwiftUI/Event/Focus/FocusedValueKey.swift +++ b/Sources/OpenSwiftUI/Event/Focus/FocusedValueKey.swift @@ -2,45 +2,98 @@ // FocusedValueKey.swift // OpenSwiftUI // -// Audited for iOS 15.5 -// Status: Complete +// Audited for 6.5.4 +// Status: WIP import OpenSwiftUICore +@available(OpenSwiftUI_v2_0, *) +@propertyWrapper +public struct FocusedValue: DynamicProperty { + @usableFromInline + @frozen + internal enum Content { + case keyPath(KeyPath) + case value(Value?) + } + + @usableFromInline + internal var content: FocusedValue.Content + + public init(_ keyPath: KeyPath) { + _openSwiftUIUnimplementedFailure() + } + + @inlinable + public var wrappedValue: Value? { + if case .value(let value) = content { + return value + } else { + return nil + } + } + + public static func _makeProperty( + in buffer: inout _DynamicPropertyBuffer, + container: _GraphValue, + fieldOffset: Int, + inputs: inout _GraphInputs + ) { + _openSwiftUIUnimplementedFailure() + } +} + +@available(*, unavailable) +extension FocusedValue: Sendable {} + +@available(*, unavailable) +extension FocusedValue.Content: Sendable {} + /// A protocol for identifier types used when publishing and observing focused /// values. /// /// Unlike ``EnvironmentKey``, `FocusedValueKey` has no default value /// requirement, because the default value for a key is always `nil`. +@available(OpenSwiftUI_v2_0, *) public protocol FocusedValueKey { associatedtype Value } +/// A collection of state exported by the focused view and its ancestors. +@available(OpenSwiftUI_v2_0, *) public struct FocusedValues { - struct StorageOptions { + var plist: PropertyList + + struct StorageOptions: OptionSet { let rawValue: UInt8 } - - var plist: PropertyList - var storageOptions: StorageOptions + + var storageOptions: FocusedValues.StorageOptions + + var navigationDepth: Int + var seed: VersionSeed - + @usableFromInline internal init() { plist = PropertyList() - storageOptions = StorageOptions(rawValue: 0) + storageOptions = [] + navigationDepth = -1 seed = .empty } - + /// Reads and writes values associated with a given focused value key. + public subscript(key: Key.Type) -> Key.Value? where Key: FocusedValueKey { - _openSwiftUIUnimplementedFailure() + get { _openSwiftUIUnimplementedFailure() } + set { _openSwiftUIUnimplementedFailure() } } } @available(*, unavailable) extension FocusedValues: Sendable {} +@available(OpenSwiftUI_v3_0, *) extension FocusedValues: Equatable { public static func == (lhs: FocusedValues, rhs: FocusedValues) -> Bool { lhs.seed.matches(rhs.seed) diff --git a/Tests/OpenSwiftUITests/Data/Environment/EnvironmentValuesOpenURLTests.swift b/Tests/OpenSwiftUITests/Data/Environment/EnvironmentValuesOpenURLTests.swift index 4c33337df..a64f0e0d9 100644 --- a/Tests/OpenSwiftUITests/Data/Environment/EnvironmentValuesOpenURLTests.swift +++ b/Tests/OpenSwiftUITests/Data/Environment/EnvironmentValuesOpenURLTests.swift @@ -1,9 +1,6 @@ // // EnvironmentValuesOpenURLTests.swift -// -// -// Created by Kyle on 2023/11/28. -// +// OpenSwiftUITests import Foundation @testable import OpenSwiftUI diff --git a/Tests/OpenSwiftUITests/Data/Environment/UIKitEnvironmentTests.swift b/Tests/OpenSwiftUITests/Data/Environment/UIKitEnvironmentTests.swift new file mode 100644 index 000000000..a672dd57e --- /dev/null +++ b/Tests/OpenSwiftUITests/Data/Environment/UIKitEnvironmentTests.swift @@ -0,0 +1,21 @@ +// +// UIKitEnvironmentTests.swift +// OpenSwiftUITests + +#if os(iOS) +@testable import OpenSwiftUI +import Testing +import UIKit + +struct UIKitEnvironmentTests { + @Test + func overrideTrait() { + let trait = UITraitCollection.current + #expect(trait.layoutDirection == .unspecified) + var environment = EnvironmentValues() + environment.layoutDirection = .rightToLeft + let newTrait = trait.byOverriding(with: environment, viewPhase: .init(), focusedValues: .init()) + #expect(newTrait.layoutDirection == .rightToLeft) + } +} +#endif