Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

63 changes: 63 additions & 0 deletions Example/OpenSwiftUIUITests/Graphic/Color/ColorUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,67 @@ struct ColorUITests {
testName: "hsb_\(name)"
)
}

@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: 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 {
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()
)
}
}
7 changes: 6 additions & 1 deletion Example/OpenSwiftUIUITests/OpenSwiftUIUITests.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
],
"defaultOptions" : {
"environmentVariableEntries" : [
{
"enabled" : false,
"key" : "SWIFTUI_PRINT_TREE",
"value" : "1"
},
{
"key" : "SNAPSHOT_REFERENCE_DIR",
"value" : "$(PROJECT_DIR)\/ReferenceImages"
Expand All @@ -25,7 +30,7 @@
{
"target" : {
"containerPath" : "container:Example.xcodeproj",
"identifier" : "275751F42DEE1456003E467C",
"identifier" : "279283B82DFF11CE00234D64",
"name" : "OpenSwiftUIUITests"
}
}
Expand Down
119 changes: 119 additions & 0 deletions Example/OpenSwiftUIUITests/UITests/AnimationDebugController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// AnimationDebugController.swift
// OpenSwiftUIUITests

import Foundation
import OpenSwiftUI_SPI

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)
}

var duration: Double {
intervals.reduce(0, +)
}
}

protocol AnimationTestView: View {
static var model: AnimationTestModel { get }
}

final class AnimationDebugController<V>: PlatformHostingController<V> 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: PlatformHostingView<V> {
view as! PlatformHostingView<V>
}

func advance(interval: Double) {
host._renderForTest(interval: interval)
}

func advanceAsync(interval: Double) -> Bool {
host._renderAsyncForTest(interval: interval)
}

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)

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
}
#endif
}

#if os(iOS) || os(visionOS)
// 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
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +22,7 @@ extension Color {
#else
import UIKit
typealias PlatformHostingController = UIHostingController
typealias PlatformHostingView = _UIHostingView
typealias PlatformViewController = UIViewController
typealias PlatformView = UIView
typealias PlatformImage = UIImage
Expand All @@ -35,7 +37,7 @@ extension Color {
let defaultSize = CGSize(width: 200, height: 200)

func openSwiftUIAssertSnapshot<V: View>(
of value: @autoclosure () throws -> V,
of value: @autoclosure () -> V,
perceptualPrecision: Float = 1,
size: CGSize = defaultSize,
named name: String? = nil,
Expand All @@ -48,7 +50,7 @@ func openSwiftUIAssertSnapshot<V: View>(
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,
Expand All @@ -62,7 +64,7 @@ func openSwiftUIAssertSnapshot<V: View>(
}

func openSwiftUIAssertSnapshot<V: View>(
of value: @autoclosure () throws -> V,
of value: @autoclosure () -> V,
as snapshotting: Snapshotting<PlatformViewController, PlatformImage>,
named name: String? = nil,
record recording: Bool? = shouldRecord,
Expand All @@ -74,7 +76,7 @@ func openSwiftUIAssertSnapshot<V: View>(
column: UInt = #column
) {
openSwiftUIAssertSnapshot(
of: PlatformHostingController(rootView: try value()),
of: PlatformHostingController(rootView: value()),
as: snapshotting,
named: name,
record: recording,
Expand All @@ -88,7 +90,7 @@ func openSwiftUIAssertSnapshot<V: View>(
}

func openSwiftUIAssertSnapshot<V: View, Format>(
of value: @autoclosure () throws -> V,
of value: @autoclosure () -> V,
as snapshotting: Snapshotting<PlatformViewController, Format>,
named name: String? = nil,
record recording: Bool? = shouldRecord,
Expand All @@ -100,7 +102,7 @@ func openSwiftUIAssertSnapshot<V: View, Format>(
column: UInt = #column
) {
openSwiftUIAssertSnapshot(
of: PlatformHostingController(rootView: try value()),
of: PlatformHostingController(rootView: value()),
as: snapshotting,
named: name,
record: recording,
Expand All @@ -114,7 +116,7 @@ func openSwiftUIAssertSnapshot<V: View, Format>(
}

private func openSwiftUIAssertSnapshot<Value, Format>(
of value: @autoclosure () throws -> Value,
of value: @autoclosure () -> Value,
as snapshotting: Snapshotting<Value, Format>,
named name: String? = nil,
record recording: Bool? = shouldRecord,
Expand All @@ -131,7 +133,7 @@ private func openSwiftUIAssertSnapshot<Value, Format>(
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,
Expand All @@ -155,3 +157,49 @@ private func openSwiftUIAssertSnapshot<Value, Format>(
)
)
}

// MARK: - Animation

func openSwiftUIAssertAnimationSnapshot<V: AnimationTestView>(
of value: @autoclosure () -> V,
precision: Float = 1,
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())
let model = V.model
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)_\(model.intervals.count).\(Int(size.width))x\(Int(size.height))",
record: recording,
timeout: timeout,
fileID: fileID,
file: filePath,
testName: testName,
line: line,
column: column
)
}
}
Loading
Loading