You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.12 16:15:51