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

使用ViewInspector异步测试SwiftUI视图超时问题排查

问题:Swift Package测试中ViewInspector的didAppear闭包未执行导致超时

使用Xcode 15.0.1构建Swift Package(非项目),测试中调用wait时出现超时,且test.on(\.didAppear)的闭包完全未执行。包内其他测试用相同逻辑可正常运行,本次测试用onChange模拟业务更新机制,但测试始终超时失败。

Package.swift配置

// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "MyLibrary",
    platforms: [
        .iOS(.v16)
    ],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(name: "MyLibrary", targets: ["MyLibrary"])
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
        .package(url: "https://github.com/realm/SwiftLint.git", from: "0.53.0"),
        .package(url: "https://github.com/nalexn/ViewInspector.git", from: "0.9.8")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "MyLibrary",
            dependencies: [],
            resources: [
                .process("Media.xcassets")
            ],
            plugins: [
                .plugin(name: "SwiftLintPlugin", package: "SwiftLint")
            ]
        ),
        .testTarget(
            name: "MyLibraryTests",
            dependencies: ["MyLibrary", "ViewInspector"])
    ]
)

测试代码

import SwiftUI
import ViewInspector
import XCTest
@testable import MyLibrary

final class MyTests: XCTestCase {
    
    struct TestView: View {
        @Binding var text: String
        @State var display: String = ""

        internal var didAppear: ((Self) -> Void)?

        init(text: String) {
            self._text = Binding<String>.init(wrappedValue: text)
        }

        var body: some View {
            HStack {
                Text("Hello")
                Text(display)
            }
            .onChange(of: text, perform: { value in
                self.display = value
            })
        }
    }

    func testTest() throws {
        var test = TestView(text: "")

        let exp = test.on(\.didAppear) { view in
            try view.hStack().callOnChange(newValue: "World")
            XCTAssertNotNil(try view.hStack().find(text: "World"))
        }

        ViewHosting.host(view: test)
        wait(for: [exp], timeout: 1.0) // <- Fails here with a timeout
    }
 }

解决方案

问题根源

  1. TestView没有触发didAppear回调的逻辑:ViewInspector的on(\.didAppear)依赖视图的onAppear生命周期触发,但你的视图未添加onAppear修饰符来调用自定义的didAppear闭包。
  2. 视图值类型传递问题:ViewHosting.host接收的是视图的副本,原变量上设置的闭包不会作用到被托管的视图实例上。

修复步骤

1. 给TestView添加onAppear触发逻辑

修改TestView的body,添加onAppear修饰符调用自定义闭包:

var body: some View {
    HStack {
        Text("Hello")
        Text(display)
    }
    .onChange(of: text, perform: { value in
        self.display = value
    })
    .onAppear {
        didAppear?(self) // 视图出现时触发回调
    }
}

2. 调整测试代码绑定闭包到托管实例

使用ViewInspector的原生API监听onAppear,确保闭包作用到正确的视图实例:

func testTest() throws {
    let testView = TestView(text: "")
    
    // 直接用ViewInspector的onAppear API监听
    let exp = testView.inspect().onAppear { view in
        try view.hStack().callOnChange(newValue: "World")
        XCTAssertNotNil(try view.hStack().find(text: "World"))
    }
    
    ViewHosting.host(view: testView)
    wait(for: [exp], timeout: 1.0)
}

额外注意事项

  • 所有视图操作必须在主线程执行,SwiftUI视图更新是主线程绑定的,异步操作要通过DispatchQueue.main.async包裹。
  • 确认ViewInspector版本与Xcode 15.0.1兼容,若仍有问题可尝试升级到最新兼容版本。

内容的提问来源于stack exchange,提问作者Cyklist

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 20:02:41