swift-snapshot-testing
swift-snapshot-testing copied to clipboard
Testing AsyncImage from SwiftUI
Describe the bug
I would like to write a snapshot test that will test my SwiftUI view containing AsyncImage. To not be dependent on external API which holds images I added png files to my codebase and created a URL to it with Bundle(for: type(of: self)).url(forResource: "testImage", withExtension: "png").
To problem is that even with .wait strategy for assertSnapshots I'm not able to record snapshots with images, I'm only getting a placeholder.
To Repro
TestingAsyncImage.zip
duce
// And/or enter code that reproduces the behavior here.
let bundle = Bundle(for: type(of: self))
let iconUrl = bundle.url(forResource: "testImage", withExtension: "png")!
let view = ContentView(viewModel: MainViewModel(model: Model(id: Int.random(in: 0...3456), iconURL:iconUrl)))
assertSnapshots(matching: view, as: [.wait(for: 2, on: .image)])
struct Model: Identifiable {
let id: Int
let iconURL: URL
}
final class MainViewModel: ObservableObject {
var model: Model
init(model: Model) {
self.model = model
}
}
struct ContentView: View {
@ObservedObject var viewModel:
MainViewModel
var body: some View {
VStack {
AsyncImage(url: viewModel.model.iconURL) { phase in
if let image = phase.image {
image.resizable().aspectRatio(contentMode: .fill)
} else if phase.error != nil {
Color.red
} else {
Color.blue
}
}.padding()
}
}
}
Expected behavior
I would expect that adding .wait strategy for 2 seconds to assertSnapshots is long enough for AsyncImage to load the image from the URL which leads to local memory.
Screenshots

Environment
- swift-snapshot-testing version 1.11.0
- Xcode 14.2
- Swift 5.7
- OS: Simulator iOS 16.2, Mac OS 13.1
Hi @Filipsky5, I had the same problem. I solved it by overwriting AsyncImage: AsyncImage.swift
Now you can overwrite the image for the given url like this:
struct AsyncImagePreview: PreviewProvider {
static var previews: some View {
AsyncImage(url: URL(string: "http://example.com/image.png"))
.environment(\.imageProvider) { url in
guard url.absoluteString == "http://example.com/image.png"
else { return nil }
return UIImage(named: "mock")
}
}
}
I think I wrote a simple reproducer that highlights this problem after encountering it myself. I'm having the same issue testing views that have a task modifier.
It seems that wait(for:on:) isn't actually waiting as I'd expect and instead the SwiftUI view is being snapshotted as it appeared in its initial state.
import SnapshotTesting
import SwiftUI
import XCTest
struct TaskView: View {
@State private var taskStarted = false
@State private var taskFinished = false
var body: some View {
VStack {
Text("Task Started: \(taskStarted.description)")
Text("Task Finished: \(taskFinished.description)")
}
.task {
taskStarted = true
try? await Task.sleep(nanoseconds: 5_000_000_000)
taskFinished = true
}
}
}
final class SwiftUISnapshotTests: XCTestCase {
func testTaskView() throws {
let taskView = TaskView()
.fixedSize()
assertSnapshot(of: taskView, as: .wait(for: 6.0, on: .image))
}
}
Snapshot:
After the 6 second wait, the 5 second artificial task has had plenty of time to complete and so you'd expect both Task Started and Task Finished to say true yet they indicate the initial state of the view.
Update: After reading the documentation of wait(for:on:) I believe this isn't a bug but rather a misunderstanding of the purpose of wait(for:on:) paired with a current limitation of swift-snapshot-testing.
I've found a couple of other discussion that I think are all focused around this same limitation, namely #582 and #669.