get the right index onTapGesture with AsyncImage

Viewed 160

While iterating over images and loading AsyncImages, the .onTapGesture does not refer to the clicked element.

Is this due to View refresh on image loading? How to bypass this issue?


var images: [String] = [
    "https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg",
    "https://www.trinum.com/ibox/ftpcam/small_montgenevre_brousset.jpg",
    "https://www.trinum.com/ibox/ftpcam/small_montgenevre_sommet-ts-crete.jpg",
    "https://www.trinum.com/ibox/ftpcam/mega_mtgenevre_sommet-des-gondrans.jpg"
]

struct thumbnail: View {
    
    @State var mainImageUrl: String = images[0];
    
    var body: some View {
        VStack {
            AsyncImage(url: URL(string: mainImageUrl)) { image in
                image
                    .resizable().scaledToFit().frame(height: 350)
            } placeholder: {
                ProgressView()
            }.frame(height: 350).cornerRadius(10)
            HStack {
                ForEach(images, id: \.self) { imageUrl in
                    AsyncImage(url: URL(string: imageUrl)) { sourceImage in
                        sourceImage
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(width: 100, height: 100, alignment: .center)
                            .clipped()
                    } placeholder: {
                        ProgressView()
                    }.onTapGesture {
                        self.mainImageUrl = imageUrl
                    }
                }
            }
        }
    }
}
1 Answers

It sort of works if you just flip the frame and aspectRatio as in the code below. However it is very slow and you are constantly re-downloading the images whenever you click on a thumbnail. The last image is specially slow.

 struct ContentView: View {
    var body: some View {
        Thumbnail()
    }
}

struct Thumbnail: View {
    
    var images: [String] = [
        "https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg",
        "https://www.trinum.com/ibox/ftpcam/small_montgenevre_brousset.jpg",
        "https://www.trinum.com/ibox/ftpcam/small_montgenevre_sommet-ts-crete.jpg",
        "https://www.trinum.com/ibox/ftpcam/mega_mtgenevre_sommet-des-gondrans.jpg"
    ]

    @State var mainImageUrl: String = "https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg"
    
    var body: some View {
        VStack {
            AsyncImage(url: URL(string: mainImageUrl)) { image in
                image.resizable().scaledToFit().frame(height: 350)
            } placeholder: {
                ProgressView()
            }.frame(height: 350).cornerRadius(10)
            HStack  { 
                ForEach(images, id: \.self) { imageUrl in
                    AsyncImage(url: URL(string: imageUrl)) { sourceImage in
                        sourceImage
                            .resizable()
                            .frame(width: 100, height: 100)   // <--- here
                            .aspectRatio(contentMode: .fill)  // <--- here
                            .clipped()
                    } placeholder: {
                        ProgressView()
                    }.onTapGesture {
                        self.mainImageUrl = imageUrl
                    }
                }
            }
        }
    }
}

IMHO, a better way is to use a different approach to avoid the constant downloading of images. You could download the pictures in parallel only once, using swift async/await concurrency. Such as in this code:

struct Thumbnail: View {
    @StateObject var loader = ImageLoader()
    @State var selectedPhoto: PhotoImg?

    var body: some View {
        VStack {
            if loader.images.count < 1 {
                ProgressView()
            } else {
                Image(uiImage: selectedPhoto?.image ?? UIImage(systemName: "smiley")!)
                    .frame(height: 350).cornerRadius(10)
                ScrollView {
                    HStack (spacing: 10) {
                        ForEach(loader.images) { photo in
                            Image(uiImage: photo.image)
                                .resizable()
                                .frame(width: 100, height: 100)
                                .aspectRatio(contentMode: .fill)
                                .onTapGesture {
                                    selectedPhoto = photo
                                }
                        }
                    }
                }
                .onAppear {
                    if let first = loader.images.first {
                        selectedPhoto = first
                    }
                }
            }
        }
        .task {
            await loader.loadParallel()
        }
    }
    
}

class ImageLoader: ObservableObject {
    @Published var images: [PhotoImg] = []
    
    let urls: [String] = [
        "https://www.trinum.com/ibox/ftpcam/small_montgenevre_eglise.jpg",
        "https://www.trinum.com/ibox/ftpcam/small_montgenevre_brousset.jpg",
        "https://www.trinum.com/ibox/ftpcam/small_montgenevre_sommet-ts-crete.jpg",
        "https://www.trinum.com/ibox/ftpcam/mega_mtgenevre_sommet-des-gondrans.jpg"
    ]

    func loadParallel() async {
        return await withTaskGroup(of: (String, UIImage).self) { group in
            for str in urls {
                if let url = URL(string: str) {
                    group.addTask { await (url.absoluteString, self.loadImage(url: url)) }
                }
            }
            for await result in group {
                DispatchQueue.main.async {
                    self.images.append(PhotoImg(url: result.0, image: result.1))
                }
            }
        }
    }
    
    private func loadImage(url: URL) async -> UIImage {
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            if let img = UIImage(data: data) { return img }
        }
        catch { print(error) }
        return UIImage()
    }
}

struct PhotoImg: Identifiable, Hashable {
    let id = UUID()
    var url: String
    var image: UIImage
}
Related