使用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 } }
解决方案
问题根源
TestView没有触发didAppear回调的逻辑:ViewInspector的on(\.didAppear)依赖视图的onAppear生命周期触发,但你的视图未添加onAppear修饰符来调用自定义的didAppear闭包。- 视图值类型传递问题:
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
相关产品推荐
相关产品推荐

