diff --git a/Sources/OpenSwiftUI/Integration/UIKit/AnyUIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/AnyUIHostingView.swift similarity index 100% rename from Sources/OpenSwiftUI/Integration/UIKit/AnyUIHostingView.swift rename to Sources/OpenSwiftUI/Integration/Hosting/UIKit/AnyUIHostingView.swift diff --git a/Sources/OpenSwiftUI/Integration/UIKit/UIGraphicsView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIGraphicsView.swift similarity index 100% rename from Sources/OpenSwiftUI/Integration/UIKit/UIGraphicsView.swift rename to Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIGraphicsView.swift diff --git a/Sources/OpenSwiftUI/Integration/UIKit/UIHostingController.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingController.swift similarity index 92% rename from Sources/OpenSwiftUI/Integration/UIKit/UIHostingController.swift rename to Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingController.swift index e6a43c518..654fc8c50 100644 --- a/Sources/OpenSwiftUI/Integration/UIKit/UIHostingController.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingController.swift @@ -47,10 +47,15 @@ open class UIHostingController : UIViewController where Content : View get { host.rootView } _modify { yield &host.rootView } } + + public func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) { + host._forEachIdentifiedView(body: body) + } } @available(macOS, unavailable) extension UIHostingController: _UIHostingViewable where Content == AnyView { + } @available(macOS, unavailable) diff --git a/Sources/OpenSwiftUI/Integration/UIKit/UIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingView.swift similarity index 92% rename from Sources/OpenSwiftUI/Integration/UIKit/UIHostingView.swift rename to Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingView.swift index 7d0eddec3..d3e77ffea 100644 --- a/Sources/OpenSwiftUI/Integration/UIKit/UIHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingView.swift @@ -7,7 +7,9 @@ // ID: FAF0B683EB49BE9BABC9009857940A1E #if os(iOS) -@_spi(ForOpenSwiftUIOnly) public import OpenSwiftUICore +@_spi(ForOpenSwiftUIOnly) +@_spi(Private) +public import OpenSwiftUICore public import UIKit @available(macOS, unavailable) @@ -151,6 +153,19 @@ open class _UIHostingView: UIView where Content: View { // TODO func clearUpdateTimer() { } + + func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) { + let tree = preferenceValue(_IdentifiedViewsKey.self) + let adjustment = { [weak self](rect: inout CGRect) in + guard let self else { return } + rect = convert(rect, from: nil) + } + tree.forEach { proxy in + var proxy = proxy + proxy.adjustment = adjustment + body(proxy) + } + } } extension _UIHostingView: ViewRendererHost { diff --git a/Sources/OpenSwiftUI/Integration/UIKit/UIHostingViewable.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingViewable.swift similarity index 87% rename from Sources/OpenSwiftUI/Integration/UIKit/UIHostingViewable.swift rename to Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingViewable.swift index 0c1fa3311..a6e607c24 100644 --- a/Sources/OpenSwiftUI/Integration/UIKit/UIHostingViewable.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/UIHostingViewable.swift @@ -13,7 +13,7 @@ import Foundation public protocol _UIHostingViewable: AnyObject { // var rootView: AnyView { get set } // func _render(seconds: Double) -// func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) + func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) // func sizeThatFits(in size: CGSize) -> CGSize // var _disableSafeArea: Bool { get set } //// var _rendererConfiguration: _RendererConfiguration { get set } diff --git a/Sources/OpenSwiftUI/Test/TestApp.swift b/Sources/OpenSwiftUI/Test/TestApp.swift index f2bfab83a..1fd057d90 100644 --- a/Sources/OpenSwiftUI/Test/TestApp.swift +++ b/Sources/OpenSwiftUI/Test/TestApp.swift @@ -6,6 +6,9 @@ // Status: WIP // ID: A519B5B95CA8FF4E3445832668F0B2D2 +@_spi(Testing) +import OpenSwiftUICore + public struct _TestApp { public init() { preconditionFailure("TODO") diff --git a/Sources/OpenSwiftUI/Test/TestIDView.swift b/Sources/OpenSwiftUI/Test/TestIDView.swift deleted file mode 100644 index f643870bf..000000000 --- a/Sources/OpenSwiftUI/Test/TestIDView.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// TestIDView.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: WIP - -import OpenGraphShims -import OpenSwiftUICore - -struct TestIDView: PrimitiveView, UnaryView { - var content: Content - var id: ID - - init(content: Content, id: ID) { - self.content = content - self.id = id - } - - static func _makeView(view: _GraphValue>, inputs: _ViewInputs) -> _ViewOutputs { - // Use IdentifiedView here - preconditionFailure("TODO") - } -} - -// TODO -extension TestIDView { - struct IdentifiedView { - @Attribute - var view: TestIDView - var id: ID? - } -} - -extension View { - func testID(_ id: ID) -> some View { - TestIDView(content: self, id: id) - } -} diff --git a/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewTree.swift b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewTree.swift new file mode 100644 index 000000000..203ea989a --- /dev/null +++ b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewTree.swift @@ -0,0 +1,28 @@ +// +// IdentifiedViewTree.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: Complete + +public enum _IdentifiedViewTree { + case empty + case proxy(_IdentifiedViewProxy) + case array([_IdentifiedViewTree]) + + public func forEach(_ body: (_IdentifiedViewProxy) -> Void) { + switch self { + case .empty: + break + case let .proxy(proxy): + body(proxy) + case let .array(array): + for treeElement in array { + treeElement.forEach(body) + } + } + } +} + +@available(*, unavailable) +extension _IdentifiedViewTree: Sendable {} diff --git a/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewsKey.swift b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewsKey.swift new file mode 100644 index 000000000..9b6def7b8 --- /dev/null +++ b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewsKey.swift @@ -0,0 +1,39 @@ +// +// IdentifiedViewsKey.swift +// OpenSwiftUI +// +// Audited for iOS 18.0 +// Status: Complete + +@_spi(Private) +public import OpenSwiftUICore + +public struct _IdentifiedViewsKey { + public typealias Value = _IdentifiedViewTree + + public static let defaultValue: _IdentifiedViewTree = .empty + + public static func reduce(value: inout _IdentifiedViewTree, nextValue: () -> _IdentifiedViewTree) { + let newValue = nextValue() + switch (value, newValue) { + case (_, .empty): + break + case (.empty, _): + value = newValue + case let (.proxy(oldProxy), .proxy(newProxy)): + value = .array([.proxy(oldProxy)] + [.proxy(newProxy)]) + case let (.array(oldArray), .proxy(newProxy)): + value = .array(oldArray + [.proxy(newProxy)]) + case let (.proxy(oldProxy), .array(newArray)): + value = .array([.proxy(oldProxy)] + newArray) + case let (.array(oldArray), .array(newArray)): + value = .array(oldArray + newArray) + } + } +} + +@available(*, unavailable) +extension _IdentifiedViewsKey: Sendable {} + +@_spi(Private) +extension _IdentifiedViewsKey: HostPreferenceKey {} diff --git a/Sources/OpenSwiftUICore/Data/Preference/PreferenceKey.swift b/Sources/OpenSwiftUICore/Data/Preference/PreferenceKey.swift index 8421ffafd..d47a530ff 100644 --- a/Sources/OpenSwiftUICore/Data/Preference/PreferenceKey.swift +++ b/Sources/OpenSwiftUICore/Data/Preference/PreferenceKey.swift @@ -36,12 +36,19 @@ public protocol PreferenceKey { /// - nextValue: A closure that returns the next value in the sequence. static func reduce(value: inout Value, nextValue: () -> Value) + /// If true `reduce()` will also see preference values for views + /// that have active removal transitions. The default + /// implementation returns false. static var _includesRemovedValues: Bool { get } + /// If true the preference may be read via the renderer host API. + /// Defaults to false. If true `_includesRemovedValues` should be + /// false. static var _isReadableByHost: Bool { get } } extension PreferenceKey where Value: ExpressibleByNilLiteral { + /// Let nil-expressible values default-initialize to nil. public static var defaultValue: Value { Value(nilLiteral: ()) } } diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift index bcfdc64cd..9f51ef6a1 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift @@ -176,7 +176,9 @@ extension ViewRendererHost { @_spi(Private) public func preferenceValue(_ key: K.Type) -> K.Value where K: HostPreferenceKey { - preconditionFailure("TODO") + updateViewGraph { graph in + graph.preferenceValue(key) + } } package func idealSize() -> CGSize { preconditionFailure("TODO") } diff --git a/Sources/OpenSwiftUICore/View/IdentifiedView/IdentifiedViewProxy.swift b/Sources/OpenSwiftUICore/View/IdentifiedView/IdentifiedViewProxy.swift new file mode 100644 index 000000000..f6b84e66d --- /dev/null +++ b/Sources/OpenSwiftUICore/View/IdentifiedView/IdentifiedViewProxy.swift @@ -0,0 +1,51 @@ +// +// IdentifiedViewProxy.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +public import Foundation + +public struct _IdentifiedViewProxy { + public var identifier: AnyHashable + package var size: CGSize + package var position: CGPoint + package var transform: ViewTransform + package var adjustment: ((inout CGRect) -> ())? + package var accessibilityNodeStorage: Any? + package var platform: _IdentifiedViewProxy.Platform + + package init(identifier: AnyHashable, size: CGSize, position: CGPoint, transform: ViewTransform, accessibilityNode: Any?, platform: _IdentifiedViewProxy.Platform) { + self.identifier = identifier + self.size = size + self.position = position + self.transform = transform + self.accessibilityNodeStorage = accessibilityNode + self.platform = platform + } + + public var boundingRect: CGRect { + var rect = CGRect(origin: .zero, size: size) + rect.convert(to: .global, transform: transform.withPosition(position)) + adjustment?(&rect) + return rect + } +} + +@available(*, unavailable) +extension _IdentifiedViewProxy: Sendable {} + +package struct IdentifiedViewPlatformInputs { + package init(inputs: _ViewInputs, outputs: _ViewOutputs) {} +} + +extension _IdentifiedViewProxy { + package struct Platform { + package init(_ inputs: IdentifiedViewPlatformInputs) {} + } +} + +package protocol IdentifierProvider { + func matchesIdentifier(_ identifier: I) -> Bool where I: Hashable +} diff --git a/Sources/OpenSwiftUICore/View/IdentifiedViewProxy.swift b/Sources/OpenSwiftUICore/View/IdentifiedViewProxy.swift deleted file mode 100644 index 309491135..000000000 --- a/Sources/OpenSwiftUICore/View/IdentifiedViewProxy.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -public struct _IdentifiedViewProxy { - var identifier: AnyHashable - var size: CGSize - var position: CGPoint - var transform: ViewTransform - var adjustment: (inout CGRect) -> ()? -// var accessibilityNodeStorage: AccessibilityNodeProxy? -} diff --git a/Sources/OpenSwiftUICore/View/Input/ViewOutputs.swift b/Sources/OpenSwiftUICore/View/Input/ViewOutputs.swift index acc22ffce..6a99fedd1 100644 --- a/Sources/OpenSwiftUICore/View/Input/ViewOutputs.swift +++ b/Sources/OpenSwiftUICore/View/Input/ViewOutputs.swift @@ -13,7 +13,7 @@ public struct _ViewOutputs { private var _layoutComputer: OptionalAttribute - init() { + package init() { preferences = PreferencesOutputs() _layoutComputer = OptionalAttribute() } diff --git a/Sources/OpenSwiftUICore/View/Test/TestIDView.swift b/Sources/OpenSwiftUICore/View/Test/TestIDView.swift new file mode 100644 index 000000000..22cebae05 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Test/TestIDView.swift @@ -0,0 +1,63 @@ +// +// TestIDView.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: WIP +// ID: CC151E1A36B4405FF56CDABA5D46BF1E + +import OpenGraphShims + +@_spi(Testing) +extension View { + nonisolated public func testID(_ id: ID) -> TestIDView where ID : Hashable { + TestIDView(content: self, id: id) + } +} + +@_spi(Testing) +@MainActor +@preconcurrency +public struct TestIDView: PrimitiveView, UnaryView where Content: View, ID: Hashable { + public var content: Content + public var id: ID + + nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + fatalError() + } + + public typealias Body = Never + + private struct IdentifiedView: StatefulRule, AsyncAttribute, IdentifierProvider, CustomStringConvertible { + @Attribute var view: TestIDView + var id: ID? + + init(view: Attribute, id: ID?) { + self._view = view + self.id = id + } + + // TODO + typealias Value = TestIDView + + mutating func updateValue() { + // TODO: id = view.id + } + + func matchesIdentifier(_ identifier: I) -> Bool where I: Hashable { + compareValues(id, identifier as? ID) + } + + var description: String { + if let id { + "ID: \(id)" + } else { + "ID" + } + } + } +} + +@_spi(Testing) +@available(*, unavailable) +extension TestIDView: Sendable {} diff --git a/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/View/IdentifiedView/IdentifiedViewProxyTests.swift b/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/View/IdentifiedView/IdentifiedViewProxyTests.swift new file mode 100644 index 000000000..cd1740922 --- /dev/null +++ b/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/View/IdentifiedView/IdentifiedViewProxyTests.swift @@ -0,0 +1,26 @@ +// +// IdentifiedViewProxyTests.swift +// OpenSwiftUICompatibilityTests + +import Testing +#if os(iOS) +import UIKit +#endif + +@MainActor +struct IdentifiedViewProxyTests { + @Test + func boundingRect() async { + #if os(iOS) && OPENSWIFTUI_COMPATIBILITY_TEST // FIXME: add _identified modifier + let identifier = "Test" + let hosting = UIHostingController(rootView: AnyView(EmptyView())._identified(by: identifier)) + await confirmation { @MainActor confirmation in + hosting._forEachIdentifiedView { proxy in + confirmation() + #expect(proxy.identifier == AnyHashable(identifier)) + #expect(proxy.boundingRect == .zero) + } + } + #endif + } +} diff --git a/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/View/IdentifiedView/IdentifiedViewTreeTests.swift b/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/View/IdentifiedView/IdentifiedViewTreeTests.swift new file mode 100644 index 000000000..3f3e830a2 --- /dev/null +++ b/Tests/OpenSwiftUICompatibilityTests/SwiftUICore/View/IdentifiedView/IdentifiedViewTreeTests.swift @@ -0,0 +1,17 @@ +// +// IdentifiedViewTreeTests.swift +// OpenSwiftUICompatibilityTests + +import Testing + +struct IdentifiedViewTreeTests { + @Test + func forEachEmpty() async { + let tree = _IdentifiedViewTree.empty + await confirmation(expectedCount: 0) { confirm in + tree.forEach { _ in + confirm() + } + } + } +} diff --git a/Tests/OpenSwiftUITests/View/IdentifiedView/IdentifiedViewTreeTests.swift b/Tests/OpenSwiftUITests/View/IdentifiedView/IdentifiedViewTreeTests.swift new file mode 100644 index 000000000..d4d3f22a9 --- /dev/null +++ b/Tests/OpenSwiftUITests/View/IdentifiedView/IdentifiedViewTreeTests.swift @@ -0,0 +1,60 @@ +// +// IdentifiedViewTreeTests.swift +// OpenSwiftUITests + +import Testing +import OpenSwiftUI +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore +#if os(iOS) +import UIKit +#endif + +#if canImport(Darwin) +struct IdentifiedViewTreeTests { + private func helper(identifier: AnyHashable) -> _IdentifiedViewProxy { + return _IdentifiedViewProxy( + identifier: identifier, + size: .zero, + position: .zero, + transform: .init(), + accessibilityNode: nil, + platform: .init(.init(inputs: .invalidInputs(.invalid), outputs: _ViewOutputs())) + ) + } + + @Test + func forEachProxy() async { + let tree = _IdentifiedViewTree.proxy(helper(identifier: "1")) + await confirmation(expectedCount: 1) { confirm in + tree.forEach { _ in + confirm() + } + } + } + + @Test + func forEachArray() async { + let tree = _IdentifiedViewTree.array([ + .proxy(helper(identifier: "1")), + .empty, + .array([.proxy(helper(identifier: "2"))]) + ]) + await confirmation(expectedCount: 2) { confirm in + tree.forEach { _ in + confirm() + } + } + } + + @Test + func forEachEmpty() async { + let tree = _IdentifiedViewTree.empty + await confirmation(expectedCount: 0) { confirm in + tree.forEach { _ in + confirm() + } + } + } +} +#endif