如何使用swift-snapshot-testing测试异步Wrapped HStack视图
问题描述
我实现了支持自动换行的 Wrapped HStack 组件,该组件采用异步逻辑计算自身高度,直接做快照测试时结果异常。查阅PointFree异步快照测试相关内容后,没有找到可落地的解决方法。
目标是使用 SwiftUI Snapshot Testing 库,对包含异步布局逻辑的 Wrapped HStack 完成正确的快照测试。
我最初参考公开示例编写的测试代码如下,运行后生成的快照为全空白。
初始测试代码
func test_wrappingHStack() { let view = ScrollView { VStack { Text("Title").font(.headline) WrappingHStack(models: ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4", "Apple", "Google", "Amazon", "Microsoft", "Oracle", "Facebook"]) { str in Text(str) } Button("Click me") {} } } assertSnapshot(matching: view.toViewController(), as: .image, record: true) } extension SwiftUI.View { func toViewController() -> UIViewController { let viewController = UIHostingController(rootView: self) viewController.view.frame = UIScreen.main.bounds return viewController } }
异常表现
运行测试后输出的快照为全空白图片:
解决方案
快照空白的核心原因:Wrapped HStack 的高度计算是异步执行的,默认的快照触发时机早于视图布局完成时间,此时内容还未完成渲染。
按以下逻辑调整测试流程即可修复:
- 先给
UIHostingController托管的视图设定固定宽度(和测试设备屏幕宽度一致),给一个足够大的初始高度触发首轮布局 - 让当前RunLoop运行极短时间(0.1秒左右),等待异步高度计算完成、SwiftUI完成视图状态更新
- 调用
sizeThatFits获取视图实际需要的高度,更新视图的frame为最终的准确尺寸 - 再留出极短的等待时间让视图完成最终布局,之后再触发快照断言
修复后的可用测试代码
import XCTest import SnapshotTesting import SwiftUI func test_wrappingHStack() { let testView = ScrollView { VStack { Text("Title").font(.headline) WrappingHStack(models: ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4", "Apple", "Google", "Amazon", "Microsoft", "Oracle", "Facebook"]) { str in Text(str) } Button("Click me") {} } } // 初始化托管控制器 let hostingController = UIHostingController(rootView: testView) // 设定测试用固定宽度,和目标测试设备屏幕宽度一致 let testWidth = UIScreen.main.bounds.width // 先给足够大的初始高度,触发首轮布局计算 hostingController.view.frame = CGRect(x: 0, y: 0, width: testWidth, height: 10000) // 等待异步布局计算完成 RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) // 计算视图实际需要的高度 let fittingSize = CGSize(width: testWidth, height: UIView.layoutFittingCompressedSize.height) let actualHeight = hostingController.view.sizeThatFits(fittingSize).height // 更新为最终准确尺寸 hostingController.view.frame = CGRect(x: 0, y: 0, width: testWidth, height: actualHeight) // 等待最终布局生效 RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05)) // 触发快照断言 assertSnapshot( matching: hostingController, as: .image(precision: 0.99, perceptualPrecision: 0.98), record: false ) }
注意事项
- 不要直接在View扩展里给托管控制器的view硬编码全屏bounds,包含ScrollView和异步布局的视图需要主动计算实际尺寸,否则会出现内容未撑开的问题
- 0.1~0.15秒的RunLoop等待时间足够覆盖SwiftUI一轮布局更新的周期,不会明显拖慢测试速度
- 如果你使用的WrappedHStack是通过GeometryReader在布局阶段读取尺寸、再异步更新高度的,可以适当把等待时间调整到0.2秒,确保状态更新已经触发视图重绘
- 测试时建议固定运行的模拟器设备型号,避免不同设备屏幕宽度差异导致换行位置变化,造成快照匹配失败
内容的提问来源于stack exchange,提问作者skywalkerdude
相关产品推荐
相关产品推荐

