From d2390cf9c0cb37f58ea8263ec585ea5e50bfa356 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 8 Nov 2025 18:59:11 +0800 Subject: [PATCH 1/9] Update folder structure --- .../{Shared => UITests}/SnapshotTesting+Testing.swift | 0 .../{Shared => UITests}/SnapshotTesting+XCTest.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Example/OpenSwiftUIUITests/{Shared => UITests}/SnapshotTesting+Testing.swift (100%) rename Example/OpenSwiftUIUITests/{Shared => UITests}/SnapshotTesting+XCTest.swift (100%) diff --git a/Example/OpenSwiftUIUITests/Shared/SnapshotTesting+Testing.swift b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift similarity index 100% rename from Example/OpenSwiftUIUITests/Shared/SnapshotTesting+Testing.swift rename to Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift diff --git a/Example/OpenSwiftUIUITests/Shared/SnapshotTesting+XCTest.swift b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+XCTest.swift similarity index 100% rename from Example/OpenSwiftUIUITests/Shared/SnapshotTesting+XCTest.swift rename to Example/OpenSwiftUIUITests/UITests/SnapshotTesting+XCTest.swift From c01753e1970e8e8ff7fdd9f733cfa5668ec0aea7 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 9 Nov 2025 11:10:59 +0800 Subject: [PATCH 2/9] Fix try throws issue of Snapshot function --- .../UITests/SnapshotTesting+Testing.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift index 65c0741d7..b307c71cf 100644 --- a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift +++ b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift @@ -35,7 +35,7 @@ extension Color { let defaultSize = CGSize(width: 200, height: 200) func openSwiftUIAssertSnapshot( - of value: @autoclosure () throws -> V, + of value: @autoclosure () -> V, perceptualPrecision: Float = 1, size: CGSize = defaultSize, named name: String? = nil, @@ -48,7 +48,7 @@ func openSwiftUIAssertSnapshot( column: UInt = #column ) { openSwiftUIAssertSnapshot( - of: PlatformHostingController(rootView: try value()), + of: PlatformHostingController(rootView: value()), as: .image(perceptualPrecision: perceptualPrecision, size: size), named: (name.map { ".\($0)" } ?? "") + "\(Int(size.width))x\(Int(size.height))", record: recording, @@ -62,7 +62,7 @@ func openSwiftUIAssertSnapshot( } func openSwiftUIAssertSnapshot( - of value: @autoclosure () throws -> V, + of value: @autoclosure () -> V, as snapshotting: Snapshotting, named name: String? = nil, record recording: Bool? = shouldRecord, @@ -74,7 +74,7 @@ func openSwiftUIAssertSnapshot( column: UInt = #column ) { openSwiftUIAssertSnapshot( - of: PlatformHostingController(rootView: try value()), + of: PlatformHostingController(rootView: value()), as: snapshotting, named: name, record: recording, @@ -88,7 +88,7 @@ func openSwiftUIAssertSnapshot( } func openSwiftUIAssertSnapshot( - of value: @autoclosure () throws -> V, + of value: @autoclosure () -> V, as snapshotting: Snapshotting, named name: String? = nil, record recording: Bool? = shouldRecord, @@ -100,7 +100,7 @@ func openSwiftUIAssertSnapshot( column: UInt = #column ) { openSwiftUIAssertSnapshot( - of: PlatformHostingController(rootView: try value()), + of: PlatformHostingController(rootView: value()), as: snapshotting, named: name, record: recording, @@ -114,7 +114,7 @@ func openSwiftUIAssertSnapshot( } private func openSwiftUIAssertSnapshot( - of value: @autoclosure () throws -> Value, + of value: @autoclosure () -> Value, as snapshotting: Snapshotting, named name: String? = nil, record recording: Bool? = shouldRecord, @@ -131,7 +131,7 @@ private func openSwiftUIAssertSnapshot( let os = "iOS_Simulator" #endif let snapshotDirectory = ProcessInfo.processInfo.environment["SNAPSHOT_REFERENCE_DIR"]! + "/\(os)/" + fileID.description - let failure = try verifySnapshot( + let failure = verifySnapshot( of: value(), as: snapshotting, named: name, From d1c5d1dea0652cedf0b8e4d817be6895bb0f0018 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 9 Nov 2025 12:15:19 +0800 Subject: [PATCH 3/9] Add AnimationTests --- .../Color/ColorAnimationExampleUITests.swift | 33 --------------- .../Graphic/Color/ColorUITests.swift | 22 ++++++++++ .../UITests/AnimationDebugController.swift | 40 +++++++++++++++++++ .../UITests/SnapshotTesting+Testing.swift | 35 ++++++++++++++++ 4 files changed, 97 insertions(+), 33 deletions(-) delete mode 100644 Example/OpenSwiftUIUITests/Graphic/Color/ColorAnimationExampleUITests.swift create mode 100644 Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift diff --git a/Example/OpenSwiftUIUITests/Graphic/Color/ColorAnimationExampleUITests.swift b/Example/OpenSwiftUIUITests/Graphic/Color/ColorAnimationExampleUITests.swift deleted file mode 100644 index 2d4b3889b..000000000 --- a/Example/OpenSwiftUIUITests/Graphic/Color/ColorAnimationExampleUITests.swift +++ /dev/null @@ -1,33 +0,0 @@ - // - // ColorAnimationExampleUITests.swift - // OpenSwiftUIUITests - - import Testing - import SnapshotTesting - - @MainActor - @Suite(.snapshots(record: .never, diffTool: diffTool)) - struct ColorAnimationExampleUITests { - @Test - func colorAnimationExample() async { - struct ContentView: View { - @State private var showRed = false - var body: some View { - VStack { - Color(platformColor: showRed ? .red : .blue) - .frame(width: showRed ? 200 : 400, height: showRed ? 200 : 400) - } - .animation(.easeInOut(duration: 2), value: showRed) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - withAnimation { - - showRed.toggle() - } - } - } - } - } - openSwiftUIAssertSnapshot(of: ContentView()) - } - } diff --git a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift index 3e1a9033f..b5b35b2f2 100644 --- a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift +++ b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift @@ -57,4 +57,26 @@ struct ColorUITests { testName: "hsb_\(name)" ) } + + @Test + func colorAnimation() { + struct ContentView: View { + @State private var showRed = false + var body: some View { + VStack { + Color(platformColor: showRed ? .red : .blue) + .frame(width: showRed ? 50 : 100, height: showRed ? 50 : 100) + } + .animation(.easeInOut(duration: 1), value: showRed) + .onAppear { + showRed.toggle() + } + } + } + let model = AnimationTestModel(duration: 1, count: 10) + openSwiftUIAssertAnimationSnapshot( + of: ContentView(), + model: model, + ) + } } diff --git a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift new file mode 100644 index 000000000..eccb4c6e8 --- /dev/null +++ b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift @@ -0,0 +1,40 @@ +// +// AnimationDebugController.swift +// OpenSwiftUIUITests + +import Foundation + +struct AnimationTestModel: Hashable { + var intervals: [Double] + + init(intervals: [Double]) { + self.intervals = intervals + } + + init(times: [Double]) { + intervals = zip(times.dropFirst(), times).map { $0 - $1 } + } + + init(duration: Double, count: Int) { + intervals = Array(repeating: duration / Double(count), count: count) + } +} + +final class AnimationDebugController: UIHostingController where V: View { + init(_ view: V) { + super.init(rootView: view) + } + + @MainActor + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func advance(interval: Double) { + (view as! _UIHostingView)._renderForTest(interval: interval) + } + + func advanceAsync(interval: Double) -> Bool { + (view as! _UIHostingView)._renderAsyncForTest(interval: interval) + } +} diff --git a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift index b307c71cf..23171d464 100644 --- a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift +++ b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift @@ -155,3 +155,38 @@ private func openSwiftUIAssertSnapshot( ) ) } + +// MARK: - Animation + +func openSwiftUIAssertAnimationSnapshot( + of value: @autoclosure () -> V, + model: AnimationTestModel, + perceptualPrecision: Float = 1, + size: CGSize = defaultSize, + record recording: Bool? = shouldRecord, + timeout: TimeInterval = 5, + fileID: StaticString = #fileID, + file filePath: StaticString = #filePath, + testName: String = #function, + line: UInt = #line, + column: UInt = #column +) { + let vc = AnimationDebugController(value()) + // Flush the neccessary onAppear etc. stuff + vc.advance(interval: .zero) + model.intervals.enumerated().forEach { (index, interval) in + vc.advance(interval: interval) + openSwiftUIAssertSnapshot( + of: vc, + as: .image(perceptualPrecision: perceptualPrecision, size: size), + named: "\(index + 1)_\(model.intervals.count).\(Int(size.width))x\(Int(size.height))", + record: recording, + timeout: timeout, + fileID: fileID, + file: filePath, + testName: testName, + line: line, + column: column + ) + } +} From f9b406e71cae3b52629175bf75e853c8c3dbb6ee Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 9 Nov 2025 12:08:03 +0800 Subject: [PATCH 4/9] Add AnimationTestView --- .../Graphic/Color/ColorUITests.swift | 11 +++++++---- .../UITests/AnimationDebugController.swift | 4 ++++ .../UITests/SnapshotTesting+Testing.swift | 7 ++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift index b5b35b2f2..6a09c09ad 100644 --- a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift +++ b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift @@ -60,7 +60,11 @@ struct ColorUITests { @Test func colorAnimation() { - struct ContentView: View { + struct ContentView: AnimationTestView { + nonisolated static var model: AnimationTestModel { + AnimationTestModel(duration: 1, count: 10) + } + @State private var showRed = false var body: some View { VStack { @@ -73,10 +77,9 @@ struct ColorUITests { } } } - let model = AnimationTestModel(duration: 1, count: 10) openSwiftUIAssertAnimationSnapshot( - of: ContentView(), - model: model, + of: ContentView() +// precision: 0.8 // TODO: Maybe related with issue #340 ) } } diff --git a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift index eccb4c6e8..ebb5f73e0 100644 --- a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift +++ b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift @@ -20,6 +20,10 @@ struct AnimationTestModel: Hashable { } } +protocol AnimationTestView: View { + static var model: AnimationTestModel { get } +} + final class AnimationDebugController: UIHostingController where V: View { init(_ view: V) { super.init(rootView: view) diff --git a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift index 23171d464..0823bbc50 100644 --- a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift +++ b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift @@ -158,9 +158,9 @@ private func openSwiftUIAssertSnapshot( // MARK: - Animation -func openSwiftUIAssertAnimationSnapshot( +func openSwiftUIAssertAnimationSnapshot( of value: @autoclosure () -> V, - model: AnimationTestModel, + precision: Float = 1, perceptualPrecision: Float = 1, size: CGSize = defaultSize, record recording: Bool? = shouldRecord, @@ -174,11 +174,12 @@ func openSwiftUIAssertAnimationSnapshot( let vc = AnimationDebugController(value()) // Flush the neccessary onAppear etc. stuff vc.advance(interval: .zero) + let model = V.model model.intervals.enumerated().forEach { (index, interval) in vc.advance(interval: interval) openSwiftUIAssertSnapshot( of: vc, - as: .image(perceptualPrecision: perceptualPrecision, size: size), + as: .image(precision: precision, perceptualPrecision: perceptualPrecision, size: size), named: "\(index + 1)_\(model.intervals.count).\(Int(size.width))x\(Int(size.height))", record: recording, timeout: timeout, From db4c17e79626734a08ed607ac7a79a126b341bfa Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 9 Nov 2025 18:00:20 +0800 Subject: [PATCH 5/9] Add duration support --- Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift | 2 +- .../OpenSwiftUIUITests/UITests/AnimationDebugController.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift index 6a09c09ad..156b271a2 100644 --- a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift +++ b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift @@ -71,7 +71,7 @@ struct ColorUITests { Color(platformColor: showRed ? .red : .blue) .frame(width: showRed ? 50 : 100, height: showRed ? 50 : 100) } - .animation(.easeInOut(duration: 1), value: showRed) + .animation(.easeInOut(duration: Self.model.duration), value: showRed) .onAppear { showRed.toggle() } diff --git a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift index ebb5f73e0..0058c3dec 100644 --- a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift +++ b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift @@ -18,6 +18,10 @@ struct AnimationTestModel: Hashable { init(duration: Double, count: Int) { intervals = Array(repeating: duration / Double(count), count: count) } + + var duration: Double { + intervals.reduce(0, +) + } } protocol AnimationTestView: View { From 0f0cb2e34b3f0927441fbc155cb2c3aedc511560 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 9 Nov 2025 22:12:26 +0800 Subject: [PATCH 6/9] Update xctestplan --- Example/OpenSwiftUIUITests/OpenSwiftUIUITests.xctestplan | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Example/OpenSwiftUIUITests/OpenSwiftUIUITests.xctestplan b/Example/OpenSwiftUIUITests/OpenSwiftUIUITests.xctestplan index b13a8e513..a756e13ef 100644 --- a/Example/OpenSwiftUIUITests/OpenSwiftUIUITests.xctestplan +++ b/Example/OpenSwiftUIUITests/OpenSwiftUIUITests.xctestplan @@ -10,6 +10,11 @@ ], "defaultOptions" : { "environmentVariableEntries" : [ + { + "enabled" : false, + "key" : "SWIFTUI_PRINT_TREE", + "value" : "1" + }, { "key" : "SNAPSHOT_REFERENCE_DIR", "value" : "$(PROJECT_DIR)\/ReferenceImages" @@ -25,7 +30,7 @@ { "target" : { "containerPath" : "container:Example.xcodeproj", - "identifier" : "275751F42DEE1456003E467C", + "identifier" : "279283B82DFF11CE00234D64", "name" : "OpenSwiftUIUITests" } } From 96a2e6884ddf094d4586ace343c799dc7b433a74 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 9 Nov 2025 22:13:06 +0800 Subject: [PATCH 7/9] Update ColorUITests --- .../Graphic/Color/ColorUITests.swift | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift index 156b271a2..c2acbcd04 100644 --- a/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift +++ b/Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift @@ -59,27 +59,65 @@ struct ColorUITests { } @Test + func frameAnimation() { + struct ContentView: AnimationTestView { + nonisolated static var model: AnimationTestModel { + AnimationTestModel(duration: 1, count: 4) + } + + @State private var smaller = false + var body: some View { + Color.red + .frame(width: smaller ? 50 : 100, height: smaller ? 50 : 100) + .animation(.linear(duration: Self.model.duration), value: smaller) + .onAppear { + smaller.toggle() + } + } + } + openSwiftUIAssertAnimationSnapshot(of: ContentView()) + } + + // FIXME + @Test(.disabled("Color interpolation is not aligned with SwiftUI yet")) func colorAnimation() { struct ContentView: AnimationTestView { nonisolated static var model: AnimationTestModel { - AnimationTestModel(duration: 1, count: 10) + AnimationTestModel(duration: 1, count: 4) + } + + @State private var showRed = false + var body: some View { + Color(platformColor: showRed ? .red : .blue) + .animation(.linear(duration: Self.model.duration), value: showRed) + .onAppear { + showRed.toggle() + } + } + } + openSwiftUIAssertAnimationSnapshot(of: ContentView()) + } + + // FIXME + @Test(.disabled("Color interpolation is not aligned with SwiftUI yet")) + func frameColorAnimation() { + struct ContentView: AnimationTestView { + nonisolated static var model: AnimationTestModel { + AnimationTestModel(duration: 1, count: 4) } @State private var showRed = false var body: some View { - VStack { - Color(platformColor: showRed ? .red : .blue) - .frame(width: showRed ? 50 : 100, height: showRed ? 50 : 100) - } - .animation(.easeInOut(duration: Self.model.duration), value: showRed) - .onAppear { - showRed.toggle() - } + Color(platformColor: showRed ? .red : .blue) + .frame(width: showRed ? 50 : 100, height: showRed ? 50 : 100) + .animation(.linear(duration: Self.model.duration), value: showRed) + .onAppear { + showRed.toggle() + } } } openSwiftUIAssertAnimationSnapshot( of: ContentView() -// precision: 0.8 // TODO: Maybe related with issue #340 ) } } From 8ca37b5d31d658ceece53146f211edbd14d9475b Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 9 Nov 2025 23:28:43 +0800 Subject: [PATCH 8/9] Update AnimationDebugController for iOS --- .../UITests/AnimationDebugController.swift | 75 +++++++++++++++++-- .../UITests/SnapshotTesting+Testing.swift | 20 +++-- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift index 0058c3dec..59eddcc52 100644 --- a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift +++ b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift @@ -3,6 +3,8 @@ // OpenSwiftUIUITests import Foundation +import UIKit +import OpenSwiftUI_SPI struct AnimationTestModel: Hashable { var intervals: [Double] @@ -28,21 +30,84 @@ protocol AnimationTestView: View { static var model: AnimationTestModel { get } } -final class AnimationDebugController: UIHostingController where V: View { +final class AnimationDebugController: PlatformHostingController where V: View { init(_ view: V) { super.init(rootView: view) } - + @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + + private var host: _UIHostingView { + view as! _UIHostingView + } + func advance(interval: Double) { - (view as! _UIHostingView)._renderForTest(interval: interval) + host._renderForTest(interval: interval) } func advanceAsync(interval: Double) -> Bool { - (view as! _UIHostingView)._renderAsyncForTest(interval: interval) + host._renderAsyncForTest(interval: interval) + } + + override func viewDidLoad() { + super.viewDidLoad() + Self.hookLayoutSubviews(type(of: host)) + Self.hookDisplayLinkTimer() + } + + var disableLayoutSubview = false + + static func hookLayoutSubviews(_ cls: AnyClass?) { + let originalSelector = #selector(PlatformView.layoutSubviews) + let swizzledSelector = #selector(PlatformView.swizzled_layoutSubviews) + + guard let targetClass = cls, + let originalMethod = class_getInstanceMethod(targetClass, originalSelector), + let swizzledMethod = class_getInstanceMethod(targetClass, swizzledSelector) + else { return } + method_exchangeImplementations(originalMethod, swizzledMethod) + } + + static func hookDisplayLinkTimer() { + #if OPENSWIFTUI + let cls: AnyClass? = NSClassFromString("OpenSwiftUI.DisplayLink") + #else + let cls: AnyClass? = NSClassFromString("SwiftUI.DisplayLink") + #endif + let sel = NSSelectorFromString("displayLinkTimer:") + guard let targetClass = cls, + let originalMethod = class_getInstanceMethod(targetClass, sel), + let swizzledMethod = class_getInstanceMethod(self, #selector(swizzled_displayLinkTimerWithLink(_:))) + else { return } + method_exchangeImplementations(originalMethod, swizzledMethod) + } + + @objc + func swizzled_displayLinkTimerWithLink(_ sender: CADisplayLink) { + sender.isPaused = true + } +} + +// Avoid generic parameter casting +private protocol AnimationDebuggableController: PlatformViewController { + var disableLayoutSubview: Bool { get set } +} + +extension AnimationDebugController: AnimationDebuggableController {} + +extension PlatformView { + @objc func swizzled_layoutSubviews() { + guard let vc = _viewControllerForAncestor as? AnimationDebuggableController else { + swizzled_layoutSubviews() + return + } + guard !vc.disableLayoutSubview else { + return + } + swizzled_layoutSubviews() + vc.disableLayoutSubview = true } } diff --git a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift index 0823bbc50..f26164f0b 100644 --- a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift +++ b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift @@ -172,15 +172,25 @@ func openSwiftUIAssertAnimationSnapshot( column: UInt = #column ) { let vc = AnimationDebugController(value()) - // Flush the neccessary onAppear etc. stuff - vc.advance(interval: .zero) let model = V.model - model.intervals.enumerated().forEach { (index, interval) in - vc.advance(interval: interval) + var intervals = model.intervals + intervals.insert(.zero, at: 0) + intervals.enumerated().forEach { (index, interval) in + switch index { + case 0: + break + case 1: + vc.advance(interval: .zero) + vc.advance(interval: .zero) + vc.advance(interval: .zero) + vc.advance(interval: interval) + default: + vc.advance(interval: interval) + } openSwiftUIAssertSnapshot( of: vc, as: .image(precision: precision, perceptualPrecision: perceptualPrecision, size: size), - named: "\(index + 1)_\(model.intervals.count).\(Int(size.width))x\(Int(size.height))", + named: "\(index)_\(model.intervals.count).\(Int(size.width))x\(Int(size.height))", record: recording, timeout: timeout, fileID: fileID, From 7ff2ef0ef935e15a515e9df97ef3b6b385706e8c Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 9 Nov 2025 23:37:39 +0800 Subject: [PATCH 9/9] Fix macOS issue --- .../UITests/AnimationDebugController.swift | 12 ++++++-- .../UITests/SnapshotTesting+Testing.swift | 2 ++ .../Hosting/AppKit/View/NSHostingView.swift | 30 +++++++++++++++++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift index 59eddcc52..f4c7e1263 100644 --- a/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift +++ b/Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift @@ -3,7 +3,6 @@ // OpenSwiftUIUITests import Foundation -import UIKit import OpenSwiftUI_SPI struct AnimationTestModel: Hashable { @@ -40,8 +39,8 @@ final class AnimationDebugController: PlatformHostingController where V: V fatalError("init(coder:) has not been implemented") } - private var host: _UIHostingView { - view as! _UIHostingView + private var host: PlatformHostingView { + view as! PlatformHostingView } func advance(interval: Double) { @@ -54,12 +53,16 @@ final class AnimationDebugController: PlatformHostingController where V: V override func viewDidLoad() { super.viewDidLoad() + #if os(iOS) || os(visionOS) Self.hookLayoutSubviews(type(of: host)) Self.hookDisplayLinkTimer() + #endif } + #if os(iOS) || os(visionOS) var disableLayoutSubview = false + // Fix swift-snapshot framework snapshot will trigger uncessary _UIHostingView.layoutSubview issue static func hookLayoutSubviews(_ cls: AnyClass?) { let originalSelector = #selector(PlatformView.layoutSubviews) let swizzledSelector = #selector(PlatformView.swizzled_layoutSubviews) @@ -89,8 +92,10 @@ final class AnimationDebugController: PlatformHostingController where V: V func swizzled_displayLinkTimerWithLink(_ sender: CADisplayLink) { sender.isPaused = true } + #endif } +#if os(iOS) || os(visionOS) // Avoid generic parameter casting private protocol AnimationDebuggableController: PlatformViewController { var disableLayoutSubview: Bool { get set } @@ -111,3 +116,4 @@ extension PlatformView { vc.disableLayoutSubview = true } } +#endif diff --git a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift index f26164f0b..6df87b1b7 100644 --- a/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift +++ b/Example/OpenSwiftUIUITests/UITests/SnapshotTesting+Testing.swift @@ -9,6 +9,7 @@ import Foundation #if canImport(AppKit) import AppKit typealias PlatformHostingController = NSHostingController +typealias PlatformHostingView = NSHostingView typealias PlatformViewController = NSViewController typealias PlatformView = NSView typealias PlatformImage = NSImage @@ -21,6 +22,7 @@ extension Color { #else import UIKit typealias PlatformHostingController = UIHostingController +typealias PlatformHostingView = _UIHostingView typealias PlatformViewController = UIViewController typealias PlatformView = UIView typealias PlatformImage = UIImage diff --git a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift index dfacbfa4a..f3cbe027c 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift @@ -416,12 +416,36 @@ open class NSHostingView: NSView, XcodeViewDebugDataProvider where Cont @available(visionOS, unavailable) extension NSHostingView { public func _renderForTest(interval: Double) { - // TODO + // FIXME: Copy from iOS version + _openSwiftUIUnimplementedWarning() + + func shouldContinue() -> Bool { + if propertiesNeedingUpdate == [], !CoreTesting.needsRender { + false + } else { + times >= 0 + } + } + advanceTimeForTest(interval: interval) + canAdvanceTimeAutomatically = false + var times = 16 + repeat { + times -= 1 + CoreTesting.needsRender = false + updateGraph { host in + host.flushTransactions() + } + RunLoop.flushObservers() + render(targetTimestamp: nil) + CATransaction.flush() + } while shouldContinue() + CoreTesting.needsRender = false + canAdvanceTimeAutomatically = true } public func _renderAsyncForTest(interval: Double) -> Bool { - // TODO - false + _openSwiftUIUnimplementedWarning() + return false } }