12

I'm having trouble getting all images to show in a List using AsyncImage. When you first run the app, it seems fine but if you start scrolling, some Images aren't shown. Either the ProgressView doesn't update until the row is scrolled outside of the screen and back into view or I get an error Code=-999 "cancelled".

I can get the all the images to show and ProgressView to update correctly using a regular Image View and going through the whole setup process of downloading and showing an image. It's only when I try to use AsyncImage that all the images aren't shown. How can I get all AsyncImages to show in a List?

struct ContentView: View {
    
    @StateObject private var viewModel = ListViewModel()
    
    var body: some View {
        List {
            ForEach(viewModel.images) { photo in
                HStack {
                    AsyncImage(url: URL(string: photo.thumbnailUrl)) { phase in
                        switch phase {
                        case .success(let image):
                            image
                        case .failure(let error):
                            let _ = print(error)
                            Text("error: \(error.localizedDescription)")
                        case .empty:
                            ProgressView()
                        @unknown default:
                            fatalError()
                        }
                    }
                    
                    VStack(alignment: .leading) {
                        Text(photo.title)
                        Text(photo.thumbnailUrl)
                    }
                }
            }
        }
        .task {
            await viewModel.loadImages()
        }
    }
}

@MainActor 
class ListViewModel: ObservableObject {
    
    @Published var images: [PhotoObject] = []
    
    func loadImages() async {
        do {
            let photos = try await AsyncImageManager.downloadImages()
                images = photos
        } catch {
            print("Could load photos: \(error)")
        }
    }
    
}

struct AsyncImageManager {
    
    static func downloadImages() async throws -> [PhotoObject] {
        let url = URL(string: "https://jsonplaceholder.typicode.com/photos")!
        
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([PhotoObject].self, from: data)
    }
}

struct PhotoObject: Identifiable, Codable {
    let albumId: Int
    let id: Int
    let title: String
    let url: String
    let thumbnailUrl: String
}

enter image description here

enter image description here

enter image description here

12
  • I could not replicate your issue, works well for me. I scrolled slowly, fast then very fast, and still all is working. I'm on macos 13 Ventura, using xcode 14.0-beta4, target macOS-13 and IOS-15.6. What system are you using? Commented Jul 31, 2022 at 0:58
  • @workingdogsupportUkraine I'm on macOS Monterey 12.5, using Xcode 14.3 and deployments of macOS-12.4 and iOS-16.0. I am currently downloading Xcode 14.4. I will test it out and get back to you. Commented Jul 31, 2022 at 2:47
  • 1
    Check out Karen Prater's Loading and caching images from URL in SwiftUI - AsyncImage or custom view model logic on youtube. She talks about AsyncImage not working well with a List. You can see the issue so clearly when she runs the app. Commented Jul 31, 2022 at 4:33
  • 1
    I don't think it's on the server end. I have 2 other working examples one using combine and one without combine using a regular Image View. They don't reproduce the issue at all. It has to be AsyncImage. Commented Jul 31, 2022 at 4:44
  • 1
    I've given up on AsyncImage, Kingfisher perfectly solved my problem Commented Aug 15, 2022 at 6:32

3 Answers 3

7

This is still a bug in iOS 16. The only solution that I found is to use ScrollView + LazyVStack instead of List.

Sign up to request clarification or add additional context in comments.

2 Comments

I'm seeing the same issue. I tried using ScrollView + LazyVStack with the ForEach code inside but it still didn't work correctly. I was still seeing error cancelled in place of the image.
@Tony Did you try with another API? Since some of them have a rate limit. This solution solved it for me directly.
4

In case anyone's still looking for a workaround for this, I hacked together the following using info from the following thread:

struct FailableAsyncImage: View {
    let url: URL?
    private let isRetry: Bool

    init(url: URL?) {
        self.init(url: url, isRetry: false)
    }

    private init(url: URL?, isRetry: Bool) {
        self.url = url
        self.isRetry = isRetry
    }

    var body: some View {
        AsyncImage(url: url) { phase in
            switch phase {
            case .empty:
                ProgressView()
            case .success(let image):
                image.resizable().scaledToFit()
            case .failure(let error):
                if !isRetry && (error as? URLError)?.code == .cancelled {
                    FailableAsyncImage(url: url, isRetry: true)
                } else {
                    Image(systemName: "exclamationmark.triangle")
                }
            @unknown default:
                EmptyView()
            }
        }
    }
}

Comments

1

Did you try to add ".fixedSize()" on your AsyncImage object? Like this:

AsyncImage(
     url: URL(string: "https://XXXXX.png"),
     content: { image in
           image.resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 100, height: 100)
     },
     placeholder: {
            ProgressView()
            .frame(width: 100, height: 100)
})
.fixedSize()

2 Comments

I can't believe that this worked, it's so unintuitive, but it did! I was seeing intermittent and random cancellations (error -999) when scrolling a ScrollView{LazyVStack} with rows containing AsyncImage, especially when flicking quickly through the list. I was using iOS 18.5 device & simulator, xcode 16.4.
doesn't work. But in my case I use ScrollView+LazyVGrid

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.