AsyncImageDemo
This repo shares strange behaviour of AsyncImage in ScrollView that 2nd item's image loading is immediately cancelled. This was reported at FB13268157.
What's wrong with this?
This app showcases AsyncImage
's loading is strangely cancelled. With the below simple View
code (hidden under Details
) just loading image or showing error as Red screen,
you can see that the 2nd item's loading had an error. On Xcode, I observed this was cancelled.
struct ItemView: View {
let url: URL?
var body: some View {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.aspectRatio(contentMode: .fill)
.containerRelativeFrame([.vertical, .horizontal])
.clipped()
case .failure(let error):
let _ = print(error)
Color.red
.containerRelativeFrame([.vertical, .horizontal])
.clipped()
@unknown default:
ProgressView()
}
}
}
}
struct ContentView: View {
let urls: [URL?] = [
URL(string: "https://picsum.photos/900/1600?0"),
URL(string: "https://picsum.photos/900/1600?1"),
URL(string: "https://picsum.photos/900/1600?2"),
URL(string: "https://picsum.photos/900/1600?3"),
URL(string: "https://picsum.photos/900/1600?4"),
]
var body: some View {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(urls, id: \.self) { url in
ItemView(url: url)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.containerRelativeFrame([.horizontal, .vertical])
}
}
.scrollTargetLayout()
}
.ignoresSafeArea()
.scrollTargetBehavior(.paging)
}
}
Error message here.
Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=https://picsum.photos/900/1600?1, NSErrorFailingURLKey=https://picsum.photos/900/1600?1, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDownloadTask <83C25DAD-869D-4FAA-A778-D69C0EA0E85E>.<2>"
), _NSURLErrorFailingURLSessionTaskErrorKey=LocalDownloadTask <83C25DAD-869D-4FAA-A778-D69C0EA0E85E>.<2>, NSLocalizedDescription=cancelled}
Environment to replicate
Xcode 15.0 (15A240d) + iOS 17 simulator (iPhone 15 Pro Max) running on macOS 14.0 Sonoma (23A344)
Possible workaround
The workaround I came up was to manage the result of loading in each view having AsyncImage
and
reset and assign back the remote image URL to imageUrl
state property on every appear and disappear hook until succeeds.
struct ItemView: View {
let url: URL?
@State private var hasImageLoaded: Bool = false
@State private var imageUrl: URL? = nil
var body: some View {
AsyncImage(url: imageUrl) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.aspectRatio(contentMode: .fill)
.containerRelativeFrame([.vertical, .horizontal])
.clipped()
.onAppear {
hasImageLoaded = true
}
case .failure(let error):
let _ = print(error)
Color.red
.containerRelativeFrame([.vertical, .horizontal])
.clipped()
@unknown default:
ProgressView()
}
}
.onAppear {
if !hasImageLoaded {
imageUrl = url
}
}
.onDisappear {
if !hasImageLoaded {
imageUrl = nil
}
}
}
}