From c8c090e094e90f6908e5f4a85067fe560782241d Mon Sep 17 00:00:00 2001 From: Erik Terwan Date: Wed, 4 Dec 2024 11:34:22 +0100 Subject: [PATCH] Improve SwiftUI interopability --- MMMTestCase.podspec | 2 +- Sources/MMMTestCase/MMMTestCase.swift | 104 +++++++++++++++----------- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/MMMTestCase.podspec b/MMMTestCase.podspec index a7b3567..d36dc52 100644 --- a/MMMTestCase.podspec +++ b/MMMTestCase.podspec @@ -6,7 +6,7 @@ Pod::Spec.new do |s| s.name = "MMMTestCase" - s.version = "1.12.1" + s.version = "1.13.0" s.summary = "Our helpers for FBTestCase and XCTestCase" s.description = s.summary s.homepage = "https://github.com/mediamonks/#{s.name}" diff --git a/Sources/MMMTestCase/MMMTestCase.swift b/Sources/MMMTestCase/MMMTestCase.swift index 46f49ba..6689937 100644 --- a/Sources/MMMTestCase/MMMTestCase.swift +++ b/Sources/MMMTestCase/MMMTestCase.swift @@ -89,36 +89,6 @@ extension MMMTestCase { ) } - private class WrapperController: UIViewController { - - private let viewController: UIViewController - - public init(_ viewController: UIViewController) { - self.viewController = viewController - super.init(nibName: nil, bundle: nil) - addChild(viewController) - viewController.didMove(toParent: self) - } - - public required init?(coder: NSCoder) { fatalError() } - - public var fitSize: CGSize = .init(width: 320, height: 480) - - public private(set) lazy var container = MMMTestCaseContainer() - - override func viewDidLoad() { - super.viewDidLoad() - view.addSubview(container) - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - container.setChildView(viewController.view, size: fitSize) - let bounds = view.bounds.inset(by: view.safeAreaInsets) - container.frame = .init(origin: bounds.origin, size: container.sizeThatFits(.zero)) - } - } - private func sizeForFit(_ fit: MMMTestCaseSize) -> CGSize { switch fit { case .natural: return self.fitSize(forPresetFit: .natural) @@ -128,34 +98,80 @@ extension MMMTestCase { } } - public func verify(view: T, fit: MMMTestCaseSize = .screenWidthTableHeight, identifier: String = "", backgroundColor: UIColor? = nil) { - - let window = UIWindow() - let viewController = UIHostingController(rootView: view) - let wrapper = WrapperController(viewController) + @available(iOS 16, *) + public func verify( + view: T, + fit: MMMTestCaseSize = .screenWidthTableHeight, + identifier: String = "", + backgroundColor: UIColor? = nil + ) { - window.rootViewController = wrapper - window.windowLevel = .normal - 1 // It could be fun to watch snapshots, but let's keep older behavior. - window.isHidden = false + let controller = UIHostingController(rootView: view) + controller.sizingOptions = .intrinsicContentSize + controller.safeAreaRegions = [] + controller.view.translatesAutoresizingMaskIntoConstraints = false let fitSize = sizeForFit(fit) - wrapper.fitSize = viewController.sizeThatFits(in: fitSize) - window.setNeedsLayout() + + switch fit { + case .natural: + controller.view.setContentCompressionResistancePriority(.required, for: .horizontal) + controller.view.setContentCompressionResistancePriority(.required, for: .vertical) + controller.view.widthAnchor.constraint( + lessThanOrEqualToConstant: sizeForFit(.screenWidth).width + ).isActive = true + case .screenWidth: + controller.view.widthAnchor.constraint(equalToConstant: fitSize.width).isActive = true + controller.view.setContentCompressionResistancePriority(.required, for: .vertical) + case .screenWidthTableHeight: + controller.view.widthAnchor.constraint(equalToConstant: fitSize.width).isActive = true + controller.view.heightAnchor.constraint(equalToConstant: fitSize.height).isActive = true + case .size(let width, let height): + controller.view.widthAnchor.constraint(equalToConstant: width).isActive = true + controller.view.heightAnchor.constraint(equalToConstant: height).isActive = true + } + + controller.view.setNeedsLayout() + + controller.view.drawHierarchy(in: .init( + origin: .zero, + size: controller.view.systemLayoutSizeFitting(.zero) + ), afterScreenUpdates: true) + // We need the layout to happen naturally now. pumpRunLoopABit() - self.verifyView( - wrapper.container, + verify( + view: controller.view, + fit: fit, identifier: [ identifier, fitSize.width > 0 ? String(format: "w%.f", fitSize.width) : nil, fitSize.height > 0 ? String(format: "h%.f", fitSize.height) : nil ].compactMap { $0 }.joined(separator: "_"), - suffixes: self.referenceFolderSuffixes(), - tolerance: 0.05 + backgroundColor: backgroundColor ) } + @available(iOS 16, *) + public func verify(previews: SwiftUI._PreviewProvider.Type, fit: MMMTestCaseSize) { + for preview in previews._allPreviews { + let view = preview.content + + if let identifier = identifierFromPreview(preview) { + self.verify(view: view, fit: fit, identifier: identifier) + } else { + NSLog("No displayName provided, skipped preview snapshot") + } + } + } + + private func identifierFromPreview(_ preview: _Preview) -> String? { + preview.displayName.map { + $0.split(separator: " ").map(\.capitalized).joined(separator: "") + } + } + /// Helps generating parameter dictionaries suitable for `varyParameters` from enums supporting `CaseIterable`. public func allTestCases(_ type: T.Type) -> [String: T] { .init(uniqueKeysWithValues: