diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeList.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeList.swift new file mode 100644 index 000000000..9f7f81899 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeList.swift @@ -0,0 +1,41 @@ +import OpenGraphShims + +// FIXME +struct AccessibilityNodeList { + var nodes: [AccessibilityNode] + var version: DisplayList.Version +} + +class AccessibilityNode { + // TODO +} + +// TODO +struct AccessibilityNodeProxy { + static func makeProxyForIdentifiedView( + with list: AccessibilityNodeList?, + environment: EnvironmentValues + ) -> AccessibilityNodeProxy? { + nil + } +} + +// MARK: - AccessibilityNodesKey [6.4.41] + +struct AccessibilityNodesKey: PreferenceKey { + static let defaultValue = AccessibilityNodeList(nodes: [], version: .init()) + + static func reduce(value: inout AccessibilityNodeList, nextValue: () -> AccessibilityNodeList) { + let next = nextValue() + value.version.combine(with: next.version) + value.nodes.append(contentsOf: next.nodes) + } +} + +extension _ViewOutputs { + @inline(__always) + var accessibilityNodes: Attribute? { + get { self[AccessibilityNodesKey.self] } + set { self[AccessibilityNodesKey.self] = newValue } + } +} diff --git a/Sources/OpenSwiftUI/App/AppDelegate.swift b/Sources/OpenSwiftUI/App/AppDelegate.swift index 9b35557fc..d7383d7e8 100644 --- a/Sources/OpenSwiftUI/App/AppDelegate.swift +++ b/Sources/OpenSwiftUI/App/AppDelegate.swift @@ -9,13 +9,19 @@ #if os(iOS) import UIKit typealias DelegateBaseClass = UIResponder +typealias PlatformApplication = UIApplication +typealias PlatformApplicationDelegate = UIApplicationDelegate #elseif os(macOS) import AppKit typealias DelegateBaseClass = NSResponder +typealias PlatformApplication = NSApplication +typealias PlatformApplicationDelegate = NSApplicationDelegate #else import Foundation // FIXME: Temporarily use NSObject as a placeholder typealias DelegateBaseClass = NSObject +typealias PlatformApplication = NSObject +typealias PlatformApplicationDelegate = AnyObject #endif class AppDelegate: DelegateBaseClass { diff --git a/Sources/OpenSwiftUI/App/OpenSwiftUIApplication.swift b/Sources/OpenSwiftUI/App/OpenSwiftUIApplication.swift index ee16e1f42..5164a9444 100644 --- a/Sources/OpenSwiftUI/App/OpenSwiftUIApplication.swift +++ b/Sources/OpenSwiftUI/App/OpenSwiftUIApplication.swift @@ -33,35 +33,63 @@ import Foundation #endif func runApp(_ app: some App) -> Never { - let graph = AppGraph(app: app) - graph.startProfilingIfNecessary() - graph.instantiate() - AppGraph.shared = graph - KitRendererCommon() +// let graph = AppGraph(app: app) +// graph.startProfilingIfNecessary() +// graph.instantiate() +// AppGraph.shared = graph + Update.ensure { + KitRendererCommon(AppDelegate.self) + } } -private func KitRendererCommon() -> Never { - let argc = CommandLine.argc - let argv = CommandLine.unsafeArgv +// MARK: - runTestingApp [6.4.41] [iOS] - #if canImport(Darwin) - #if os(iOS) || os(tvOS) || os(macOS) - let principalClassName = NSStringFromClass(OpenSwiftUIApplication.self) - #endif - let delegateClassName = NSStringFromClass(AppDelegate.self) +func runTestingApp(rootView: V1, comparisonView: V2, didLaunch: @escaping (any TestHost, any TestHost) -> ()) -> Never where V1: View, V2: View { + #if os(iOS) + TestingSceneDelegate.connectCallback = { (window: UIWindow, comparisonWindow: UIWindow) in + CoreTesting.isRunning = true + let rootVC = UIHostingController(rootView: rootView) + window.rootViewController = rootVC + window.makeKeyAndVisible() + let host = rootVC.host + TestingAppDelegate.testHost = host + let comparisonVC = UIHostingController(rootView: comparisonView) + comparisonWindow.rootViewController = comparisonVC + comparisonWindow.makeKeyAndVisible() + comparisonWindow.isHidden = false + comparisonWindow.isHidden = true + let comparisonHost = comparisonVC.host + TestingAppDelegate.comparisonHost = comparisonHost + didLaunch(host, comparisonHost) + } #endif + KitRendererCommon(TestingAppDelegate.self) +} - #if os(iOS) || os(tvOS) - let code = UIApplicationMain(argc, argv, principalClassName, delegateClassName) - #elseif os(watchOS) - let code = WKApplicationMain(argc, argv, delegateClassName) - #elseif os(macOS) - // FIXME - let code = NSApplicationMain(argc, argv) - #else - let code: Int32 = 1 - #endif - exit(code) + +private func KitRendererCommon(_ delegateType: AnyObject.Type) -> Never { + let closure = { (argv: UnsafeMutablePointer?>) in + let argc = CommandLine.argc + #if canImport(Darwin) + #if os(iOS) || os(tvOS) || os(macOS) + let principalClassName = NSStringFromClass(OpenSwiftUIApplication.self) + #endif + let delegateClassName = NSStringFromClass(delegateType) + #endif + + #if os(iOS) || os(tvOS) + let code = UIApplicationMain(argc, argv, principalClassName, delegateClassName) + #elseif os(watchOS) + let code = WKApplicationMain(argc, argv, delegateClassName) + #elseif os(macOS) + // FIXME + let code = NSApplicationMain(argc, argv) + #else + let code: Int32 = 1 + #endif + return exit(code) + } + return closure(CommandLine.unsafeArgv) } #if canImport(Darwin) diff --git a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/Controller/NSHostingController.swift b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/Controller/NSHostingController.swift index 82d77896e..03079e0b3 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/Controller/NSHostingController.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/Controller/NSHostingController.swift @@ -142,7 +142,7 @@ open class NSHostingController: NSViewController where Content: View { public func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) { - host._forEachIdentifiedView(body: body) + host.forEachIdentifiedView(body: body) } diff --git a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift index 67926aea2..0de0ca70b 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift @@ -249,20 +249,6 @@ open class NSHostingView: NSView, XcodeViewDebugDataProvider where Cont // TODO } - // FIXME - 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) - } - } - package func makeViewDebugData() -> Data? { Update.ensure { _ViewDebug.serializedData(viewGraph.viewDebugData()) @@ -423,4 +409,19 @@ extension NSHostingView: HostingViewProtocol { anchor.convert(to: viewGraph.transform) } } + +extension NSHostingView/*: TestHost*/ { + 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) + } + } +} #endif diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingController.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingController.swift index f907d2e97..842850257 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingController.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/Controller/UIHostingController.swift @@ -68,7 +68,7 @@ open class UIHostingController: UIViewController where Content : View { } public func _forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) { - host._forEachIdentifiedView(body: body) + host.forEachIdentifiedView(body: body) } @available(*, deprecated, message: "Use UIHostingController/safeAreaRegions or _UIHostingView/safeAreaRegions") diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift index 03eb90460..c5ce3dd6d 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift @@ -343,7 +343,21 @@ open class _UIHostingView: UIView, XcodeViewDebugDataProvider where Con frameDidChange(oldValue: oldValue) } } - + + open override var bounds: CGRect { + get { + super.bounds + } + set { + guard allowFrameChanges else { + return + } + let oldValue = super.bounds + super.bounds = newValue + frameDidChange(oldValue: oldValue) + } + } + // TODO func setRootView(_ view: Content, transaction: Transaction) { @@ -417,20 +431,6 @@ open class _UIHostingView: UIView, XcodeViewDebugDataProvider where Con func clearUpdateTimer() { } - // FIXME - 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) - } - } - @_spi(Private) @available(iOS, deprecated, message: "Use UIHostingController/safeAreaRegions or _UIHostingView/safeAreaRegions") final public var addsKeyboardToSafeAreaInsets: Bool { @@ -706,4 +706,133 @@ extension _UIHostingView: HostingViewProtocol { } } +// MARK: - _UIHostingView + TestHost [6.4.41] + +extension _UIHostingView: TestHost { + package func setTestSize(_ size: CGSize) { + let newSize: CGSize + if size == CGSize.deviceSize { + let screenSize = UIDevice.current.screenSize + let idiom = UIDevice.current.userInterfaceIdiom + if idiom == .pad, screenSize.width < screenSize.height { + newSize = CGSize(width: screenSize.height, height: screenSize.width) + } else { + if idiom == .phone, screenSize.height < screenSize.width { + newSize = CGSize(width: screenSize.height, height: screenSize.width) + } else { + newSize = screenSize + } + } + } else { + newSize = size + } + if bounds.size != newSize { + allowFrameChanges = true + bounds.size = newSize + allowFrameChanges = false + } + } + + package func setTestSafeAreaInsets(_ insets: EdgeInsets) { + explicitSafeAreaInsets = insets + + } + + package var testSize: CGSize { bounds.size } + + package var viewCacheIsEmpty: Bool { + Update.locked { + renderer.viewCacheIsEmpty + } + } + + package func forEachIdentifiedView(body: (_IdentifiedViewProxy) -> Void) { + let tree = preferenceValue(_IdentifiedViewsKey.self) + tree.forEach { proxy in + var proxy = proxy + proxy.adjustment = { [weak self] rect in + guard let self else { return } + rect = convert(rect, from: nil) + } + body(proxy) + } + } + + package func forEachDescendantHost(body: (any TestHost) -> Void) { + forEachDescendantHost { (view: UIView) in + if let testHost = view as? any TestHost { + body(testHost) + } + } + } + + package func renderForTest(interval: Double) { + _renderForTest(interval: interval) + } + + package var attributeCountInfo: AttributeCountTestInfo { + preferenceValue(AttributeCountInfoKey.self) + } + + public func _renderForTest(interval: Double) { + func shouldContinue() -> Bool { + if propertiesNeedingUpdate == [], !CoreTesting.needRender { + false + } else { + times >= 0 + } + } + advanceTimeForTest(interval: interval) + canAdvanceTimeAutomatically = false + var times = 16 + repeat { + times -= 1 + CoreTesting.needRender = false + updateGraph { host in + host.flushTransactions() + } + RunLoop.flushObservers() + render(targetTimestamp: nil) + CATransaction.flush() + } while shouldContinue() + CoreTesting.needRender = false + canAdvanceTimeAutomatically = true + } +} + +extension UIDevice { + package var screenSize: CGSize { + let screenBounds = UIScreen.main.bounds + let screenWidth = screenBounds.width + let screenHeight = screenBounds.height + let orientation = UIDevice.current.orientation + let finalWidth: CGFloat + let finalHeight: CGFloat + switch orientation { + case .landscapeLeft, .landscapeRight: + // In landscape, swap dimensions to ensure width > height + finalWidth = max(screenWidth, screenHeight) + finalHeight = min(screenWidth, screenHeight) + case .portrait, .portraitUpsideDown: + // In portrait, keep original dimensions (height > width) + finalWidth = screenWidth + finalHeight = screenHeight + default: + // For other orientations, keep original dimensions + finalWidth = screenWidth + finalHeight = screenHeight + } + return CGSize(width: finalWidth, height: finalHeight) + } +} + +extension UIView { + func forEachDescendantHost(body: (UIView) -> Void) { + body(self) + for view in subviews { + view.forEachDescendantHost(body: body) + } + } +} + #endif diff --git a/Sources/OpenSwiftUI/Test/PerformanceTest.swift b/Sources/OpenSwiftUI/Test/PerformanceTest.swift index 7be82e5dc..cc7cbb13f 100644 --- a/Sources/OpenSwiftUI/Test/PerformanceTest.swift +++ b/Sources/OpenSwiftUI/Test/PerformanceTest.swift @@ -2,8 +2,7 @@ // PerformanceTest.swift // OpenSwiftUI // -// Audited for iOS 18.0 -// Status: WIP +// Status: Complete #if os(iOS) import UIKit @@ -12,17 +11,22 @@ import AppKit #endif import OpenSwiftUI_SPI +// MARK: - _PerformanceTest [6.4.41] + +@available(OpenSwiftUI_v1_0, *) public protocol _PerformanceTest: _Test { var name: String { get } func runTest(host: any _BenchmarkHost, options: [AnyHashable: Any]) } +@available(OpenSwiftUI_v1_0, *) extension __App { public static func _registerPerformanceTests(_ tests: [_PerformanceTest]) { TestingAppDelegate.performanceTests = tests } } +@available(OpenSwiftUI_v1_0, *) extension _BenchmarkHost { public func _started(test: _PerformanceTest) { #if os(iOS) @@ -30,7 +34,6 @@ extension _BenchmarkHost { #elseif os(macOS) NSApplication.shared.startedTest(test.name) #else - preconditionFailure("Unimplemented for other platform") #endif } @@ -40,7 +43,6 @@ extension _BenchmarkHost { #elseif os(macOS) NSApplication.shared.finishedTest(test.name) #else - preconditionFailure("Unimplemented for other platform") #endif } @@ -50,7 +52,6 @@ extension _BenchmarkHost { #elseif os(macOS) NSApplication.shared.failedTest(test.name, withFailure: nil) #else - preconditionFailure("Unimplemented for other platform") #endif } } diff --git a/Sources/OpenSwiftUI/Test/RootViewForSimplifiedApplicationProvider.swift b/Sources/OpenSwiftUI/Test/RootViewForSimplifiedApplicationProvider.swift new file mode 100644 index 000000000..fc9cacfc6 --- /dev/null +++ b/Sources/OpenSwiftUI/Test/RootViewForSimplifiedApplicationProvider.swift @@ -0,0 +1,19 @@ +// +// RootViewForSimplifiedApplicationProvider.swift +// OpenSwiftUI +// +// Status: Complete + +import Foundation + +protocol ClarityUIApplicationDelegate: PlatformApplicationDelegate { + associatedtype Body: View + + var rootViewForSimplifiedApplication: Body { get } +} + +protocol RootViewForSimplifiedApplicationProvider { + associatedtype Body: View + + var rootViewForSimplifiedApplication: Body { get } +} diff --git a/Sources/OpenSwiftUI/Test/TestApp+Test.swift b/Sources/OpenSwiftUI/Test/TestApp+Test.swift new file mode 100644 index 000000000..be87a51f4 --- /dev/null +++ b/Sources/OpenSwiftUI/Test/TestApp+Test.swift @@ -0,0 +1,64 @@ +// +// TestApp+Test.swift +// OpenSwiftUI +// +// Status: Complete + +@_spi(Testing) +public import OpenSwiftUICore +import Foundation + +// MARK: - TestApp + Test [6.4.41] + +extension _TestApp { + public func runBenchmarks(_ benchmarks: [any _Benchmark]) -> Never { + runTestingApp( + rootView: _TestApp.RootView().testID(_TestApp.rootViewIdentifier), + comparisonView: EmptyView() + ) { host, comparisonHost in + DispatchQueue.main.async { + performBenchmarks(benchmarks, with: host) + } + } + } + + func performBenchmarks(_ benchmarks: [any _Benchmark], with host: any TestHost) { + _TestApp.host = host + host.environmentOverride = _TestApp.defaultEnvironment + #if canImport(Darwin) + CFRunLoopPerformBlock( + CFRunLoopGetCurrent(), + CFRunLoopMode.commonModes.rawValue + ) { + var results: [(_Benchmark, [Double])] = [] + for benchmark in benchmarks { + benchmark.setUpTest() + results.append((benchmark, benchmark.measure(host: host))) + if enableProfiler, + let rendererhost = host as? ViewRendererHost { + rendererhost.archiveJSON(name: "\(type(of: benchmark))") + rendererhost.resetProfile() + } + benchmark.tearDownTest() + } + log(results) + exit(0) + } + #else + openSwiftUIUnimplementedFailure() + #endif + } +} + +extension _TestApp { + public func runPerformanceTests(_ tests: [any _PerformanceTest]) -> Never { + TestingAppDelegate.performanceTests = tests + runTestingApp( + rootView: _TestApp.RootView().testID(_TestApp.rootViewIdentifier), + comparisonView: EmptyView() + ) { host, comparisonHost in + _TestApp.host = host + } + } +} + diff --git a/Sources/OpenSwiftUI/Test/TestApp.swift b/Sources/OpenSwiftUI/Test/TestApp.swift index 54660abd6..04146e3d8 100644 --- a/Sources/OpenSwiftUI/Test/TestApp.swift +++ b/Sources/OpenSwiftUI/Test/TestApp.swift @@ -5,6 +5,7 @@ // Audited for iOS 18.0 // Status: WIP +@_spi(Testing) public import OpenSwiftUICore import Foundation @@ -26,16 +27,3 @@ extension _TestApp { preconditionFailure("TODO") } } - -extension _TestApp { - public func runBenchmarks(_ benchmarks: [_Benchmark]) -> Never { - let _ = RootView() - preconditionFailure("TODO") - } -} - -extension _TestApp { - public func runPerformanceTests(_ tests: [_PerformanceTest]) -> Never { - preconditionFailure("TODO") - } -} diff --git a/Sources/OpenSwiftUI/Test/TestingAppDelegate.swift b/Sources/OpenSwiftUI/Test/TestingAppDelegate.swift index 4bf5b8d0e..46b7dcf02 100644 --- a/Sources/OpenSwiftUI/Test/TestingAppDelegate.swift +++ b/Sources/OpenSwiftUI/Test/TestingAppDelegate.swift @@ -1,13 +1,47 @@ +// +// TestingSceneDelegate.swift +// OpenSwiftUI +// +// Status: Complete for iOS + #if os(iOS) import UIKit #elseif os(macOS) import AppKit #endif -class TestingAppDelegate: DelegateBaseClass { +// MARK: - TestingAppDelegate [6.4.41] [iOS] + +class TestingAppDelegate: DelegateBaseClass, PlatformApplicationDelegate { + static var testHost: (PlatformView & TestHost)? + + static var comparisonHost: (PlatformView & TestHost)? + static var performanceTests: [_PerformanceTest]? - + + static var application: PlatformApplication? + #if os(iOS) - static var connectCallback: ((UIWindow) -> Void)? + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + true + } + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + configuration.delegateClass = TestingSceneDelegate.self + return configuration + } + + // WIP + @objc + func application(_ application: UIApplication, runTest name: String, options: [AnyHashable: Any]) -> Bool { + guard let performanceTests = TestingAppDelegate.performanceTests, + let performanceTest = performanceTests.first(where: { $0.name == name }), + let host = TestingAppDelegate.testHost else { + return false + } + performanceTest.runTest(host: host, options: options) + return true + } #endif } diff --git a/Sources/OpenSwiftUI/Test/TestingSceneDelegate.swift b/Sources/OpenSwiftUI/Test/TestingSceneDelegate.swift new file mode 100644 index 000000000..a0d9898f9 --- /dev/null +++ b/Sources/OpenSwiftUI/Test/TestingSceneDelegate.swift @@ -0,0 +1,40 @@ +// +// TestingSceneDelegate.swift +// OpenSwiftUI +// +// Status: Complete for iOS + +#if os(iOS) +import UIKit + +// MARK: - TestingSceneDelegate [6.4.41] [iOS] + +class TestingSceneDelegate: DelegateBaseClass, UIWindowSceneDelegate { + var window: UIWindow? + + var comparisonWindow: UIWindow? + + static var connectCallback: ((UIWindow, UIWindow) -> Void)? + + override init() { + self.window = nil + self.comparisonWindow = nil + super.init() + } + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard window == nil, + let windowScene = scene as? UIWindowScene else { + return + } + let window = UIWindow(windowScene: windowScene) + self.window = window + let comparisonWindow = UIWindow(windowScene: windowScene) + self.comparisonWindow = comparisonWindow + guard let connectCallback = TestingSceneDelegate.connectCallback else { + return + } + connectCallback(window, comparisonWindow) + } +} +#endif diff --git a/Sources/OpenSwiftUI/Test/ViewTest.swift b/Sources/OpenSwiftUI/Test/ViewTest.swift new file mode 100644 index 000000000..1fa79fa05 --- /dev/null +++ b/Sources/OpenSwiftUI/Test/ViewTest.swift @@ -0,0 +1,317 @@ +// +// ViewTest.swift +// OpenSwiftUI +// +// Status: Complete +// ID: 1FCA4829BCDAAC91F1E6D1FB696F6642 (SwiftUI) + +public import Foundation +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore +#if os(iOS) +public import UIKit +#endif + +// MARK: - _ViewTest [6.4.41] + +@available(OpenSwiftUI_v1_0, *) +public protocol _ViewTest: _Test { + associatedtype RootView: View + + associatedtype RootStateType = Void + + func initRootView() -> Self.RootView + + func initSize() -> CGSize + + func setTestView(_ view: V) where V: View +} + +#if os(iOS) +private enum Error: Swift.Error { + case failedToReenableAnimations(String) + case failedToDismissPresentation(String) +} +#endif + +@available(OpenSwiftUI_v1_0, *) +extension _ViewTest { + public func setUpTest() { + setEnvironment(EnvironmentValues()) + setSize(initSize()) + setSafeAreaInsets(.zero) + setRooTestView(initRootView()) + withRenderOptions(.simple) { + render() + } + } + + public func tearDownTest() { + resetEvents() + setRooTestView(EmptyView()) + #if os(iOS) + func performRender() { + withRenderOptions(.simple) { + render() + } + } + UIView.performWithoutAnimation { + performRender() + } + #endif + } + + @available(OpenSwiftUI_v4_4, *) + public func tearDownTestWithError() throws { + #if os(iOS) + guard !UIView.areAnimationsEnabled else { + return + } + UIView.setAnimationsEnabled(true) + throw Error.failedToReenableAnimations(String(describing: self)) + #endif + } + + public func setTestView(_ view: V) where V: View { + setRooTestView(view) + } + + public var rootView: Self.RootView { + withRenderIfNeeded { + _TestApp.host!.viewForIdentifier( + rootViewID, + RootView.self + ) + }! + } + + private var rootViewID: Int { + findState()!.wrappedValue.id + } + + private func setRooTestView(_ view: V) where V: View { + let state = findState()! + let host = _TestApp.host! + if let viewRendererHost = host as? ViewRendererHost { + viewRendererHost.currentTimestamp = Time(seconds: ceil(viewRendererHost.currentTimestamp.seconds + 1.0)) + } + state.wrappedValue.setTestView(view) + } + + private func findState() -> Binding<_TestApp.RootView.StateType>? { + withRenderIfNeeded { + _TestApp.host!.stateForIdentifier( + _TestApp.rootViewIdentifier, + type: _TestApp.RootView.StateType.self, + in: _TestApp.RootView.self + ) + } + } + + public func viewForIdentifier( + _ identifier: I, + _ type: V.Type = V.self + ) -> V? where V: View, I: Hashable { + withRenderIfNeeded { + _TestApp.host!.viewForIdentifier(identifier, type) + } + } + + public func stateForIdentifier( + _ id: I, + type stateType: S.Type, + in viewType: V.Type + ) -> Binding? where I: Hashable, V: View { + withRenderIfNeeded { + _TestApp.host!.stateForIdentifier(id, type: stateType, in: viewType) + } + } + + private func withRenderIfNeeded(_ body: () -> V?) -> V? { + if let value = body() { + return value + } else { + _TestApp.host!.renderForTest(interval: .zero) + return body() + } + } + + public func render(seconds: Double = 1.0 / 60.0) { + let renderOptions = _TestApp.renderOptions + let host = renderOptions.contains(.comparison) ? _TestApp.comparisonHost! : _TestApp.host! + render( + host: host, + seconds: seconds, + options: renderOptions + ) + let isPostRenderRunLoop = renderOptions.contains(.postRenderRunLoop) + if isPostRenderRunLoop { + turnRunLoopIfNeeded(host: host, seconds: seconds, options: renderOptions) + } + } + + @available(OpenSwiftUI_v3_0, *) + public func renderAsync(seconds: Double = 1.0 / 60.0) -> Bool { + render(host: _TestApp.host!, seconds: seconds, options: [.async]) + } + + @available(OpenSwiftUI_v5_0, *) + public func renderRecursively(seconds: Double = 1.0 / 60.0) { + render(host: _TestApp.host!, seconds: seconds, options: [.recursive]) + } + + @discardableResult + private func render(host: any TestHost, seconds: Double, options: TestRenderOptions) -> Bool { + let isRecursive = options.contains(.recursive) + if isRecursive { + var result = true + host.forEachDescendantHost { host in + if options.contains(.async) { + if !host._renderAsyncForTest(interval: seconds) { + result = false + } + } else { + host.renderForTest(interval: seconds) + } + } + return result + } else { + if options.contains(.async) { + return host._renderAsyncForTest(interval: seconds) + } else { + host.renderForTest(interval: seconds) + return true + } + } + } + + public func initSize() -> CGSize { + CGSize(width: 100, height: 100) + } + + public func setSize(_ size: CGSize) { + _TestApp.host!.setTestSize(size) + _TestApp.comparisonHost?.setTestSize(size) + } + + func setSafeAreaInsets(_ insets: EdgeInsets) { + _TestApp.host!.setTestSafeAreaInsets(insets) + } + + public func setEnvironment(_ environment: EnvironmentValues?) { + _TestApp.setTestEnvironment(environment) + } + + #if os(iOS) + public var systemColorScheme: UIUserInterfaceStyle? { + let view = _TestApp.host! as! UIView + guard let window = view.window, + let windowScene = window.windowScene else { + return nil + } + return windowScene._systemUserInterfaceStyle + } + #endif + + public func updateEnvironment(_ body: (inout EnvironmentValues) -> Void) { + _TestApp.updateTestEnvironment(body) + } + + public func resetEvents() { + _TestApp.host!.resetTestEvents() + } + + public func loop() { + render() + let defaultMode = RunLoop.Mode.default + let commonMode = RunLoop.Mode.common + var count: UInt = 0 + let interval = 0.001 + while true { + let date = Date(timeIntervalSinceNow: interval) + if !RunLoop.current.run(mode: count & 1 == 0 ? defaultMode : commonMode, before: date) { + Thread.sleep(forTimeInterval: interval) + } + count += 1 + } + } + + public func turnRunloop(times: Int = 1) { + Swift.assert(times > 0) + let defaultMode = RunLoop.Mode.default + let commonMode = RunLoop.Mode.common + let interval = 0.001 + var times = times + while times != 0 { + times -= 1 + // let modes = [defaultMode, commonMode] + let date = Date(timeIntervalSinceNow: interval) + if !RunLoop.current.run(mode: defaultMode, before: date) { + Thread.sleep(forTimeInterval: interval) + } + } + } + + private func turnRunLoopIfNeeded(host: any TestHost, seconds: Double, options: TestRenderOptions) { + guard CoreTesting.neeedsRunLoopTurn else { + return + } + let defaultMode = RunLoop.Mode.default + let commonMode = RunLoop.Mode.common + let interval = 0.001 + var times = 17 + while CoreTesting.needRender || CoreTesting.neeedsRunLoopTurn { + // let modes = [defaultMode, commonMode] + let date = Date(timeIntervalSinceNow: interval) + if !RunLoop.current.run(mode: defaultMode, before: date) { + Thread.sleep(forTimeInterval: interval) + } + render(host: host, seconds: seconds, options: options) + times &-= 1 + if times <= 1 { + break + } + } + if CoreTesting.needRender || CoreTesting.neeedsRunLoopTurn { + Log.unitTests.log(level: .default, "Render or run loop turn needed after max iterations") + } + } +} + +extension _ViewTest { + public func rootState(type: S.Type = S.self) -> Binding { + withRenderIfNeeded { + _TestApp.host!.stateForIdentifier( + rootViewID, + type: type, + in: RootView.self + ) + }! + } + + public func rootState( + type stateType: S.Type = S.self, + in viewType: V.Type + ) -> Binding where V: View { + withRenderIfNeeded { + _TestApp.host!.stateForIdentifier( + rootViewID, + type: stateType, + in: viewType + ) + }! + } +} + +extension _ViewTest { + public func set( + _ keyPath: WritableKeyPath, + to value: V + ) { + rootState(type: RootStateType.self).wrappedValue[keyPath: keyPath] = value + } + + public func get(_ keyPath: KeyPath) -> V { + rootState(type: RootStateType.self).wrappedValue[keyPath: keyPath] + } +} diff --git a/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedModifier.swift b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedModifier.swift new file mode 100644 index 000000000..66b1b4810 --- /dev/null +++ b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedModifier.swift @@ -0,0 +1,85 @@ +// +// IdentifiedModifier.swift +// OpenSwiftUI +// +// Status: Complete +// ID: 972049776785601E5EF56C4D9DFD84DB (SwiftUI) + +import OpenGraphShims +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore + +// MARK: - _IdentifiedModifier [6.4.41] + +@available(OpenSwiftUI_v1_0, *) +@frozen +public struct _IdentifiedModifier: ViewModifier, MultiViewModifier, PrimitiveViewModifier, Equatable where Identifier: Hashable { + public var identifier: Identifier + + @inlinable + public init(identifier: Identifier) { + self.identifier = identifier + } + + nonisolated public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + var outputs = body(_Graph(), inputs) + let transform = IdentifiedViewTransform( + modifier: modifier.value, + size: inputs.animatedSize(), + position: inputs.animatedPosition(), + transform: inputs.transform, + environment: inputs.environment, + nodeList: OptionalAttribute(outputs.accessibilityNodes), + platform: IdentifiedViewPlatformInputs(inputs: inputs, outputs: outputs) + ) + outputs.preferences.makePreferenceTransformer( + inputs: inputs.preferences, + key: _IdentifiedViewsKey.self, + transform: Attribute(transform) + ) + return outputs + } +} + +@available(*, unavailable) +extension _IdentifiedModifier: Sendable {} + +@available(OpenSwiftUI_v1_0, *) +extension View { + @inlinable + @MainActor + @preconcurrency public func _identified(by identifier: I) -> some View where I: Hashable { + return modifier(_IdentifiedModifier(identifier: identifier)) + } +} + +private struct IdentifiedViewTransform: Rule, AsyncAttribute where Identifier: Hashable { + @Attribute var modifier: _IdentifiedModifier + @Attribute var size: ViewSize + @Attribute var position: ViewOrigin + @Attribute var transform: ViewTransform + @Attribute var environment: EnvironmentValues + @OptionalAttribute var nodeList: AccessibilityNodeList? + var platform: IdentifiedViewPlatformInputs + + var value: (inout _IdentifiedViewTree) -> Void { + let proxy = _IdentifiedViewProxy( + identifier: modifier.identifier, + size: size.value, + position: position, + transform: transform, + accessibilityNode: AccessibilityNodeProxy.makeProxyForIdentifiedView(with: nodeList, environment: environment), + platform: _IdentifiedViewProxy.Platform(platform) + ) + return { (tree: inout _IdentifiedViewTree) in + tree = .array([ + .proxy(proxy), + tree, + ]) + } + } +} diff --git a/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewTree.swift b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewTree.swift index 203ea989a..e6b1204a2 100644 --- a/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewTree.swift +++ b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewTree.swift @@ -2,9 +2,11 @@ // IdentifiedViewTree.swift // OpenSwiftUI // -// Audited for iOS 18.0 // Status: Complete +// MARK: - _IdentifiedViewTree [6.4.41] + +@available(OpenSwiftUI_v1_0, *) public enum _IdentifiedViewTree { case empty case proxy(_IdentifiedViewProxy) diff --git a/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewsKey.swift b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewsKey.swift index 9b6def7b8..3d934d5ca 100644 --- a/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewsKey.swift +++ b/Sources/OpenSwiftUI/View/IdentifiedView/IdentifiedViewsKey.swift @@ -2,12 +2,15 @@ // IdentifiedViewsKey.swift // OpenSwiftUI // -// Audited for iOS 18.0 // Status: Complete +// ID: 972049776785601E5EF56C4D9DFD84DB (?) @_spi(Private) public import OpenSwiftUICore +// MARK: - _IdentifiedViewsKey [6.4.41] + +@available(OpenSwiftUI_v1_0, *) public struct _IdentifiedViewsKey { public typealias Value = _IdentifiedViewTree @@ -36,4 +39,5 @@ public struct _IdentifiedViewsKey { extension _IdentifiedViewsKey: Sendable {} @_spi(Private) +@available(OpenSwiftUI_v1_0, *) extension _IdentifiedViewsKey: HostPreferenceKey {} diff --git a/Sources/OpenSwiftUICore/Data/Preference/PreferenceList.swift b/Sources/OpenSwiftUICore/Data/Preference/PreferenceList.swift index dd0f94c4e..683d4bc89 100644 --- a/Sources/OpenSwiftUICore/Data/Preference/PreferenceList.swift +++ b/Sources/OpenSwiftUICore/Data/Preference/PreferenceList.swift @@ -25,7 +25,7 @@ package struct PreferenceList: CustomStringConvertible { } } - package subscript(key: K.Type) -> Value where K: PreferenceKey{ + package subscript(key: K.Type) -> Value where K: PreferenceKey { get { guard let first, let node = first.find(key) else { diff --git a/Sources/OpenSwiftUICore/Data/Preference/PreferenceTransformModifier.swift b/Sources/OpenSwiftUICore/Data/Preference/PreferenceTransformModifier.swift new file mode 100644 index 000000000..20140d9b7 --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Preference/PreferenceTransformModifier.swift @@ -0,0 +1,80 @@ +// +// PreferenceTransformModifier.swift +// OpenSwiftUICore +// +// Status: WIP +// ID: D3405DB583003A73D556A7797845B7F4 (SwiftUICore) + +package import OpenGraphShims + +// MARK: - PreferenceTransformModifier [6.4.41] [WIP] + +@available(OpenSwiftUI_v1_0, *) +@frozen +public struct _PreferenceTransformModifier: ViewModifier, MultiViewModifier, PrimitiveViewModifier where Key: PreferenceKey { + public var transform: (inout Key.Value) -> Void + + public typealias Body = Never + + @inlinable + public init( + key _: Key.Type = Key.self, + transform: @escaping (inout Key.Value) -> Void + ) { + self.transform = transform + } + + nonisolated public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + var outputs = body(_Graph(), inputs) + outputs.preferences.makePreferenceTransformer( + inputs: inputs.preferences, + key: Key.self, + transform: modifier.value.transform + ) + return outputs + } +} + +@available(*, unavailable) +extension _PreferenceTransformModifier: Sendable {} + +extension View { + @inlinable + nonisolated public func transformPreference( + _ key: K.Type = K.self, + _ callback: @escaping (inout K.Value) -> Void + ) -> some View where K: PreferenceKey { + modifier(_PreferenceTransformModifier(transform: callback)) + } +} + +extension PreferencesOutputs { + package mutating func makePreferenceTransformer( + inputs: PreferencesInputs, + key _: K.Type, + transform: @autoclosure () -> Attribute<(inout K.Value) -> Void> + ) where K: PreferenceKey { + openSwiftUIUnimplementedWarning() + } +} + +// TODO +private struct PreferenceTransform where K: PreferenceKey { + @Attribute var transform: (inout K.Value) -> Void + @OptionalAttribute var childValue: K.Value? +} + +// TODO +private struct HostPreferencesTransform where K: PreferenceKey { + @Attribute var transform: (inout K.Value) -> Void + @Attribute var keys: Attribute + @OptionalAttribute var childValues: PreferenceValues? + var keyRequested: Bool + var wasEmpty: Bool + var delta: UInt32 + let nodeId: UInt32 +} diff --git a/Sources/OpenSwiftUICore/Data/Preference/PreferenceValues.swift b/Sources/OpenSwiftUICore/Data/Preference/PreferenceValues.swift new file mode 100644 index 000000000..f30238c6a --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Preference/PreferenceValues.swift @@ -0,0 +1,82 @@ +// +// PreferenceValues.swift +// OpenSwiftUICore + +import Foundation + +// NOTE: PreferenceValues is a replacement for PreferenceList on 6.4.41 + +// MARK: - PreferenceValues [6.4.41] [WIP] + +package struct PreferenceValues { + private var entries: [Entry] + + @inlinable + package init() { + entries = [] + } + + package struct Value { + package var value: T + package var seed: VersionSeed + + package init(value: T, seed: VersionSeed) { + self.value = value + self.seed = seed + } + } + + package subscript(key: K.Type) -> Value where K: PreferenceKey { + get { + guard let value = index(of: key).map({ (index: Int) -> Value in + entries[index][] + }) else { + return Value(value: key.defaultValue, seed: .empty) + } + return value + } + set { + let index = _index(of: key) + setValue(newValue, of: key, at: index) + } + } + + package func valueIfPresent(for key: K.Type = K.self) -> Value? where K: PreferenceKey { + index(of: key).map { (index: Int) -> Value in + entries[index][] + } + } + + private func index(of key: K.Type) -> Int? where K: PreferenceKey { + let index = _index(of: key) + guard index != entries.count, entries[index].key == key else { + return nil + } + return index + } + + private func _index(of key: any PreferenceKey.Type) -> Int { + guard !entries.isEmpty else { + return 0 + } + return entries.partitionPoint { entry in + entry.key == key + } + } + + private func setValue(_ value: Value, of key: any PreferenceKey.Type, at index: Int) { + // TODO + } +} + +extension PreferenceValues { + private struct Entry { + var key: any PreferenceKey.Type + var seed: VersionSeed + var value: Any + + subscript() -> Value { + Value(value: value as! V, seed: seed) + } + } +} diff --git a/Sources/OpenSwiftUICore/Data/Preference/TODO/_PreferenceTransformModifier.swift b/Sources/OpenSwiftUICore/Data/Preference/TODO/_PreferenceTransformModifier.swift deleted file mode 100644 index 66bdd1f82..000000000 --- a/Sources/OpenSwiftUICore/Data/Preference/TODO/_PreferenceTransformModifier.swift +++ /dev/null @@ -1,11 +0,0 @@ -@frozen -public struct _PreferenceTransformModifier: ViewModifier where Key: PreferenceKey { - public var transform: (inout Key.Value) -> Void - public typealias Body = Never - @inlinable public init(key _: Key.Type = Key.self, transform: @escaping (inout Key.Value) -> Void) { - self.transform = transform - } -// public static func _makeView(modifier: _GraphValue<_PreferenceTransformModifier>, inputs: _ViewInputs, body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs) -> _ViewOutputs { -// -// } -} diff --git a/Sources/OpenSwiftUICore/Data/Preference/TODO/_PreferenceWritingModifier.swift b/Sources/OpenSwiftUICore/Data/Preference/TODO/_PreferenceWritingModifier.swift index dd6140989..f4f5643ba 100644 --- a/Sources/OpenSwiftUICore/Data/Preference/TODO/_PreferenceWritingModifier.swift +++ b/Sources/OpenSwiftUICore/Data/Preference/TODO/_PreferenceWritingModifier.swift @@ -12,3 +12,11 @@ public struct _PreferenceWritingModifier: ViewModifier where Key: Preferenc //extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable { // public static func == (a: _PreferenceWritingModifier, b: _PreferenceWritingModifier) -> Bool //} + +extension View { + /// Sets a value for the given preference. + @inlinable + public func preference(key: K.Type = K.self, value: K.Value) -> some View where K : PreferenceKey { + modifier(_PreferenceWritingModifier(value: value)) + } +} diff --git a/Sources/OpenSwiftUICore/Data/Preference/View_Preference.swift b/Sources/OpenSwiftUICore/Data/Preference/View_Preference.swift deleted file mode 100644 index 2df2b2667..000000000 --- a/Sources/OpenSwiftUICore/Data/Preference/View_Preference.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// View_Preference.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Complete -// ID: 6C396F98EFDD04A6B58F2F9112448013 - -extension View { - /// Sets a value for the given preference. - @inlinable - public func preference(key: K.Type = K.self, value: K.Value) -> some View where K : PreferenceKey { - modifier(_PreferenceWritingModifier(value: value)) - } -} - - -extension View { - /// Applies a transformation to a preference value. - @inlinable - public func transformPreference(_ key: K.Type = K.self, _ callback: @escaping (inout K.Value) -> Void) -> some View where K : PreferenceKey { - modifier(_PreferenceTransformModifier(transform: callback)) - } -} diff --git a/Sources/OpenSwiftUICore/Event/Event.swift b/Sources/OpenSwiftUICore/Event/Event.swift new file mode 100644 index 000000000..c625848ec --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Event.swift @@ -0,0 +1,4 @@ +// FIXME + +package struct EventID: Hashable {} +package protocol EventType {} diff --git a/Sources/OpenSwiftUICore/Graph/GraphReuse.swift b/Sources/OpenSwiftUICore/Graph/GraphReuse.swift index 684ff50e6..e9fa89b0c 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphReuse.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphReuse.swift @@ -180,11 +180,7 @@ extension Log { static func graphReuse(_ message: @autoclosure () -> String) { if EnableGraphReuseLogging.isEnabled { let message = message() - #if OPENSWIFTUI_SWIFT_LOG - graphReuseLog.log(level: .info, "\(message)") - #else - graphReuseLog.log("\(message)") - #endif + graphReuseLog.log(level: .default, "\(message)") } } } diff --git a/Sources/OpenSwiftUICore/Graphic/Color/SystemColors.swift b/Sources/OpenSwiftUICore/Graphic/Color/SystemColors.swift index 99d716a78..92e1513f7 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/SystemColors.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/SystemColors.swift @@ -263,6 +263,11 @@ extension EnvironmentValues { var systemColorDefinition: SystemColorDefinitionType { self[SystemColorDefinitionKey.self] } + + @inline(__always) + mutating func setTestSystemColorDefinition() { + self[SystemColorDefinitionKey.self] = SystemColorDefinitionType(base: TestingSystemColorDefinition.self) + } } // MARK: - CoreUIDefaultSystemColorDefinition diff --git a/Sources/OpenSwiftUICore/Log/Logging.swift b/Sources/OpenSwiftUICore/Log/Logging.swift index 26757bea2..101ad0450 100644 --- a/Sources/OpenSwiftUICore/Log/Logging.swift +++ b/Sources/OpenSwiftUICore/Log/Logging.swift @@ -20,6 +20,15 @@ extension Logger { self = logger } } + +extension Logger.Level { + #if DEBUG + package static let `default`: Logger.Level = .debug + #else + package static let `default`: Logger.Level = .info + #endif +} + #else public import os.log @@ -165,6 +174,9 @@ package enum Log { package static let archivedButton: Logger = Logger(subsystem: subsystem, category: "ArchivedButton") package static let archivedPlaybackButton: Logger = Logger(subsystem: subsystem, category: "ArchivedPlaybackButton") package static let metadataExtraction: Logger = Logger(subsystem: subsystem, category: "MetadataExtraction") + + // NOTE: Added in 6.4.41 + package static let unitTests: Logger = Logger(subsystem: subsystem, category: "UnitTests") } @available(*, unavailable) @@ -198,3 +210,31 @@ extension os.OSLog { static var runtimeIssuesLog: os.OSLog = OSLog(subsystem: "com.apple.runtime-issues", category: "OpenSwiftUI") } #endif + +// MARK: - OpenSwiftUI dev addition Log API + +@_transparent +package func openSwiftUIUnimplementedFailure(_ function: String = #function, file: StaticString = #fileID, line: UInt = #line) -> Never { + preconditionFailure("TODO", file: file, line: line) + +} + +@_transparent +package func openSwiftUIPlatformUnimplementedFailure(_ function: String = #function, file: StaticString = #fileID, line: UInt = #line) -> Never { + preconditionFailure("TODO", file: file, line: line) +} + +@_transparent +package func openSwiftUIUnimplementedWarning(_ function: String = #function, file: StaticString = #fileID, line: UInt = #line) { + print("[Warning]: \(function) is unimplemented") + #if DEBUG && OPENSWIFTUI_DEVELOPMENT + openSwiftUIUnimplementedFailure(function, file: file, line: line) + #endif +} + +package func openSwiftUIPlatformUnimplementedWarning(_ function: String = #function, file: StaticString = #fileID, line: UInt = #line) { + print("[Warning]: \(function) is unimplemented on this platform") + #if DEBUG && OPENSWIFTUI_DEVELOPMENT + openSwiftUIPlatformUnimplementedFailure(function, file: file, line: line) + #endif +} diff --git a/Sources/OpenSwiftUICore/Test/AttributeCountTestInfo.swift b/Sources/OpenSwiftUICore/Test/AttributeCountTestInfo.swift new file mode 100644 index 000000000..354704fb9 --- /dev/null +++ b/Sources/OpenSwiftUICore/Test/AttributeCountTestInfo.swift @@ -0,0 +1,35 @@ +// +// AttributeCountTestInfo.swift +// OpenSwiftUICore +// +// Status: Complete + +// MARK: - AttributeCountTestInfo [6.4.41] + +package struct AttributeCountTestInfo: Equatable { + var attributeCounts: [String: UInt32] = [:] + var updateCounts: [String: UInt32] = [:] + var changeCounts: [String: UInt32] = [:] + var history: [String: UInt32] = [:] + + mutating func merge(_ other: AttributeCountTestInfo) { + attributeCounts.merge(other.attributeCounts) { $0 + $1 } + updateCounts.merge(other.updateCounts) { $0 + $1 } + changeCounts.merge(other.changeCounts) { $0 + $1 } + history.merge(other.history) { $0 + $1 } + } +} + +// MARK: - AttributeCountInfoKey [6.4.41] + +package struct AttributeCountInfoKey: HostPreferenceKey { + package static let defaultValue = AttributeCountTestInfo() + + package static func reduce( + value: inout AttributeCountTestInfo, + nextValue: () -> AttributeCountTestInfo + ) { + value.merge(nextValue()) + } +} + diff --git a/Sources/OpenSwiftUICore/Test/Benchmark.swift b/Sources/OpenSwiftUICore/Test/Benchmark.swift index 0cb4edb23..1475acb17 100644 --- a/Sources/OpenSwiftUICore/Test/Benchmark.swift +++ b/Sources/OpenSwiftUICore/Test/Benchmark.swift @@ -81,7 +81,6 @@ extension _BenchmarkHost { } } -// WIP package func summarize(_ measurements: [(any _Benchmark, [Double])]) -> String { let benchmarkData = measurements.map { (String(describing: $0.0), $0.1) } diff --git a/Sources/OpenSwiftUICore/Test/CoreTesting.swift b/Sources/OpenSwiftUICore/Test/CoreTesting.swift new file mode 100644 index 000000000..7e0219b35 --- /dev/null +++ b/Sources/OpenSwiftUICore/Test/CoreTesting.swift @@ -0,0 +1,21 @@ +// +// CoreTesting.swift +// OpenSwiftUICore +// +// Status: Complete + +// MARK: - CoreTesting [6.4.41] + +package enum CoreTesting { + package static var isRunning: Bool = false + + package static var needRender: Bool = false + + package static var neeedsRunLoopTurn: Bool { + false + } + + package static func pushNeedsRunLoopTurn() {} + + package static func popNeedsRunLoopTurn() {} +} diff --git a/Sources/OpenSwiftUICore/Test/Test.swift b/Sources/OpenSwiftUICore/Test/Test.swift index 0689916c0..2d54ecf8b 100644 --- a/Sources/OpenSwiftUICore/Test/Test.swift +++ b/Sources/OpenSwiftUICore/Test/Test.swift @@ -18,6 +18,38 @@ extension _Test { public func tearDownTestWithError() throws {} } +// MARK: - TestRenderOptions [6.4.41] + +package struct TestRenderOptions: OptionSet { + package let rawValue: UInt64 + + package init(rawValue: UInt64) { + self.rawValue = rawValue + } + + package static let `default`: TestRenderOptions = [.recursive, .postRenderRunLoop] + + package static var current: TestRenderOptions { _TestApp.renderOptions } + + package static let simple: TestRenderOptions = .init(rawValue: 0) + + package static let recursive: TestRenderOptions = .init(rawValue: 1 << 0) + + package static let async: TestRenderOptions = .init(rawValue: 1 << 1) + + package static let postRenderRunLoop: TestRenderOptions = .init(rawValue: 1 << 2) + + package static let comparison: TestRenderOptions = .init(rawValue: 1 << 3) +} + +package func withRenderOptions(_ options: TestRenderOptions, _ body: () -> Void) { + let previous = _TestApp.renderOptions + defer { _TestApp.renderOptions = previous } + body() +} + +// MARK: - TestIntents [6.4.41] + package struct TestIntents: OptionSet { package let rawValue: UInt64 @@ -118,6 +150,8 @@ package struct TestIntents: OptionSet { package static let ignorePlatformSpecificStyling: TestIntents = [.ignoreGeometry, .ignoreCornerRadius, .ignoreOpacity, .ignoreCompositingFilters] } +// MARK: - PlatformViewTestProperties [6.4.41] + package struct PlatformViewTestProperties: OptionSet { package let rawValue: UInt64 diff --git a/Sources/OpenSwiftUICore/Test/TestApp.swift b/Sources/OpenSwiftUICore/Test/TestApp.swift index 5fb3c8a2e..01981aff3 100644 --- a/Sources/OpenSwiftUICore/Test/TestApp.swift +++ b/Sources/OpenSwiftUICore/Test/TestApp.swift @@ -2,11 +2,12 @@ // TestApp.swift // OpenSwiftUICore // -// Audited for iOS 18.0 -// Status: WIP +// Status: Complete // ID: A519B5B95CA8FF4E3445832668F0B2D2 (SwiftUI) // ID: E1A97A5CD5A5467396F8BB461CB26984 (SwiftUICore) +// MARK: _TestApp [6.4.41] [WIP for defaultEnvironment] + @available(OpenSwiftUI_v1_0, *) public struct _TestApp { package static var rootViewIdentifier: some Hashable { 0 } @@ -45,10 +46,15 @@ public struct _TestApp { package static let defaultEnvironment: EnvironmentValues = { var environment = EnvironmentValues() CoreGlue2.shared.configureDefaultEnvironment(&environment) + // TODO: Font: "HelveticaNeue" + environment.displayScale = 2.0 + environment.setTestSystemColorDefinition() // TODO return environment }() + package static var renderOptions: TestRenderOptions = .default + /// Initialize a `_TestApp` for running tests. public init() { CoreGlue2.shared.initializeTestApp() @@ -73,13 +79,21 @@ public struct _TestApp { package static var environmentOverride: EnvironmentValues? package static func setTestEnvironment(_ environment: EnvironmentValues?) { - // TODO - host?.environmentOverride = environment - comparisonHost?.environmentOverride = environment + if let environment { + var env = defaultEnvironment + env.plist.override(with: environment.plist) + environmentOverride = env + } else { + environmentOverride = nil + } + host?.invalidateProperties(.environment, mayDeferUpdate: true) + comparisonHost?.invalidateProperties(.environment, mayDeferUpdate: true) } package static func updateTestEnvironment(_ body: (inout EnvironmentValues) -> Void) { - // TODO + var environment = EnvironmentValues() + body(&environment) + setTestEnvironment(environment) } package func setSemantics(_ version: String) { diff --git a/Sources/OpenSwiftUICore/Test/TestHost.swift b/Sources/OpenSwiftUICore/Test/TestHost.swift index aabdd2b52..be1d05e99 100644 --- a/Sources/OpenSwiftUICore/Test/TestHost.swift +++ b/Sources/OpenSwiftUICore/Test/TestHost.swift @@ -2,17 +2,20 @@ // TestHost.swift // OpenSwiftUICore // -// Audited for iOS 18.0 -// Status: WIP +// Status: Complete package import Foundation +// MARK: - TestHost [6.4.41] + package protocol TestHost: _BenchmarkHost { func setTestSize(_ size: CGSize) func setTestSafeAreaInsets(_ insets: EdgeInsets) - // func sendTestEvents(_ events: [EventID: any EventType]) + var testSize: CGSize { get } + + func sendTestEvents(_ events: [EventID: any EventType]) func resetTestEvents() @@ -34,19 +37,18 @@ package protocol TestHost: _BenchmarkHost { var accessibilityEnabled: Bool { get set } - var hasActivePresentation: Bool { get } - - func dismissActivePresentations() - - // var attributeCountInfo: AttributeCountTestInfo { get } + var attributeCountInfo: AttributeCountTestInfo { get } } extension TestHost { - package var hasActivePresentation: Bool { false } - - package func dismissActivePresentations() {} + package func testIntentsChanged(before: TestIntents, after: TestIntents) {} } -extension TestHost { - package func testIntentsChanged(before: TestIntents, after: TestIntents) {} +extension CGSize { + package static var deviceSize: CGSize { + CGSize( + width: Double.greatestFiniteMagnitude, + height: Double.greatestFiniteMagnitude + ) + } } diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift index 312542eb7..e3e9c46e0 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewRendererHost.swift @@ -347,10 +347,15 @@ package let hostingViewCoordinateSpace: CoordinateSpace.ID = .init() // package func setInheritedPhase(_ phase: _GestureInputs.InheritedPhase) //} -//extension ViewRendererHost { -// package func sendTestEvents(_ events: [EventID : any EventType]) -// package func resetTestEvents() -//} +extension ViewRendererHost { + package func sendTestEvents(_ events: [EventID : any EventType]) { + openSwiftUIUnimplementedWarning() + } + + package func resetTestEvents() { + openSwiftUIUnimplementedWarning() + } +} // MARK: - ViewGraph + viewRendererHost diff --git a/Sources/OpenSwiftUICore/View/IdentifiedView/IdentifiedViewProxy.swift b/Sources/OpenSwiftUICore/View/IdentifiedView/IdentifiedViewProxy.swift index f6b84e66d..8952e3712 100644 --- a/Sources/OpenSwiftUICore/View/IdentifiedView/IdentifiedViewProxy.swift +++ b/Sources/OpenSwiftUICore/View/IdentifiedView/IdentifiedViewProxy.swift @@ -2,11 +2,14 @@ // IdentifiedViewProxy.swift // OpenSwiftUICore // -// Audited for iOS 18.0 // Status: Complete public import Foundation +import OpenGraphShims +// MARK: - _IdentifiedViewProxy [6.4.41] + +@available(OpenSwiftUI_v1_0, *) public struct _IdentifiedViewProxy { public var identifier: AnyHashable package var size: CGSize @@ -36,6 +39,8 @@ public struct _IdentifiedViewProxy { @available(*, unavailable) extension _IdentifiedViewProxy: Sendable {} +// MARK: - IdentifiedViewPlatformInputs [6.4.41] + package struct IdentifiedViewPlatformInputs { package init(inputs: _ViewInputs, outputs: _ViewOutputs) {} } @@ -46,6 +51,79 @@ extension _IdentifiedViewProxy { } } +// MARK: - IdentifierProvider [6.4.41] + package protocol IdentifierProvider { func matchesIdentifier(_ identifier: I) -> Bool where I: Hashable } + +extension _BenchmarkHost { + public func viewForIdentifier( + _ identifier: I, + _ type: V.Type + ) -> V? where I: Hashable, V: View { + guard let render = self as? ViewRendererHost else { + return nil + } + return render.findIdentifier(identifier, root: nil) { attribute in + var predicate = ViewValuePredicate(view: nil) + _ = attribute.breadthFirstSearch(options: ._2) { anyAttribute in + predicate.apply(to: anyAttribute) + } + return predicate.view + } + } + + public func stateForIdentifier( + _ id: I, + type stateType: S.Type, + in viewType: V.Type + ) -> Binding? where I: Hashable, V: View { + guard let render = self as? ViewRendererHost else { + return nil + } + return render.stateForIdentifier(id, type: stateType, in: viewType) + } +} + +extension ViewRendererHost { + func stateForIdentifier( + _ id: I, + type stateType: S.Type, + in viewType: V.Type + ) -> Binding? where I: Hashable, V: View { + findIdentifier(id, root: nil) { attribute in + var predicate = ViewStatePredicate() + _ = attribute.breadthFirstSearch(options: ._2) { anyAttribute in + predicate.apply(to: anyAttribute) + } + return predicate.state + } + } + + func findIdentifier( + _ identifier: I, + root: AnyAttribute?, + filter: (AnyAttribute) -> V? + ) -> V? where I: Hashable { + let root = root ?? viewGraph.rootView + var v: V? = nil + _ = root.breadthFirstSearch(options: ._2) { attribute in + func project(type: T.Type) -> Bool { + let bodyValue = attribute._bodyPointer.assumingMemoryBound(to: type).pointee + guard let provider = bodyValue as? IdentifierProvider else { + return false + } + guard provider.matchesIdentifier(identifier), + let value = filter(attribute) else { + return false + } + v = value + return true + } + return _openExistential(attribute._bodyType, do: project) + } + return v + } + +} diff --git a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h index fd5f001a4..e96e2d929 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h +++ b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.h @@ -39,6 +39,10 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) CGFloat _pointsPerInch_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(_pointsPerInch); @end +@interface UIWindowScene (OpenSwiftUI_SPI) +@property (nonatomic, readonly) UIUserInterfaceStyle _systemUserInterfaceStyle_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(_systemUserInterfaceStyle); +@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 6effbf16c..5d6300580 100644 --- a/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m +++ b/Sources/OpenSwiftUI_SPI/Shims/UIKit/UIKit_Private.m @@ -68,4 +68,12 @@ - (CGFloat)_pointsPerInch_openswiftui_safe_wrapper { } @end +@implementation UIWindowScene (OpenSwiftUI_SPI) +- (UIUserInterfaceStyle) _systemUserInterfaceStyle_openswiftui_safe_wrapper { + OPENSWIFTUI_SAFE_WRAPPER_IMP(UIUserInterfaceStyle, @"_systemUserInterfaceStyle", UIUserInterfaceStyleUnspecified); + return func(self, selector); +} +@end + + #endif /* UIKit.h */