swiftswiftuiswiftui-listlazyvgrid

Using VStack with AsyncImage inside LazyVStack causes images to reload on scrolling


I'm trying to display a long list of images with titles using the new AsyncImage in SwiftUI. I noticed that when I put VStack around AsyncImage and scroll through images it's reloading images every time I scroll up or down. When there's no VStack I see no reloading and images seem to stay cached.

Is there a way to make VStack not to reload images on scrolling so I can add text under each image?

Here's a working example. Try scrolling with and without VStack.

import SwiftUI

struct TestView: View {

    let url = URL(string: "https://picsum.photos/200/300")

    let columns: [GridItem] = [.init(.fixed(110)),.init(.fixed(110)),.init(.fixed(110))]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns) {
                ForEach(0..<20) { _ in
                    // VStack here causes to images to reload on scrolling
                    VStack {
                        AsyncImage(url: url) { image in
                            image
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                        } placeholder: {
                            Image(systemName: "photo")
                                .imageScale(.large)
                                .frame(width: 110, height: 110)
                        }
                    }
                }
            }
        }
    }
}

Solution

  • I fully agree with you that this is a weird bug. I would guess that the reason it's happening has something to do with the way LazyVGrid chooses to layout views, and that using a VStack here gives it the impression there is more than one view to show. This is a poor job on Apple's part, but this is how I solved it: just put the VStacks internal to the AsyncImage. I'm not entirely sure what the original error is, but I do know that this fixes it.

    struct MyTestView: View {
        let url = URL(string: "https://picsum.photos/200/300")
        let columns: [GridItem] = [.init(.fixed(110)),.init(.fixed(110)),.init(.fixed(110))]
    
        var body: some View {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(0..<20) { i in
                        AsyncImage(url: url) { image in
                            VStack {
                                image
                                    .resizable()
                                    .aspectRatio(contentMode: .fit)
                                
                                Text("label \(i)")
                            }
                        } placeholder: {
                            VStack {
                                Image(systemName: "photo")
                                    .imageScale(.large)
                                    .frame(width: 110, height: 110)
                                
                                Text("image broken")
                            }
                        }
                    }
                }
            }
        }
    }