ARTICLE AD BOX
Trying to download some images before displaying them in a SwiftUI list to avoid displaying placeholders. Prefetching the images in batches of 10, but waiting only for the first batch to finish before displaying the list.
public struct ImagePrefetcher { /// Prefetches images into `URLCache.imageCache` so that `CachedAsyncImage` can display them instantly. /// /// Downloads are processed in batches of `batchSize` concurrent requests. The function awaits /// the first batch and then returns, while remaining batches continue downloading in a detached /// background task. URLs already present in the cache are skipped. /// /// - Parameters: /// - urls: The image URLs to prefetch. /// - batchSize: Number of concurrent downloads per batch. Defaults to `10`. /// - Returns: URLs successfully downloaded in the first batch. @discardableResult public static func prefetch(_ urls: [URL], batchSize: Int = 10) async -> [URL] { guard !urls.isEmpty else { return [] } let session = URLSession.shared let cache = URLCache.imageCache let firstEnd = min(batchSize, urls.count) let firstUrls = Array(urls[0..<firstEnd]) let downloaded = await fetchBatch(urls: firstUrls, session: session, cache: cache) if firstEnd < urls.count { let remaining = Array(urls[firstEnd...]) Task.detached(priority: .utility) { for batch in stride(from: 0, to: remaining.count, by: batchSize) { let end = min(batch + batchSize, remaining.count) await fetchBatch(urls: Array(remaining[batch..<end]), session: session, cache: cache) } } } return downloaded } @discardableResult private static func fetchBatch(urls: [URL], session: URLSession, cache: URLCache) async -> [URL] { await withTaskGroup(of: URL.self) { group in for url in urls { let request = URLRequest(url: url) if cache.cachedResponse(for: request) != nil { print("DEBUG image already in cache \(url)") continue } group.addTask { if let (data, response) = try? await session.data(for: request), let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) { cache.storeCachedResponse( CachedURLResponse(response: response, data: data), for: request ) print("DEBUG image downloaded \(url)") return url } return URL(string: "example.com")! } } var downloaded: [URL] = [] for await url in group { downloaded.append(url) } return downloaded } } } func prefetchImages(items: [FolderItem]) async { let urls = items.compactMap { $0.image?.largestImage() } let results = await ImagePrefetcher.prefetch(urls, batchSize: 10) print("DEBUG list shown \(results)") }The prefetch function returns before downloading the images.
Console prints:
All the images are downloaded successfully
For reference, this sample code works correctly:
func randomSum() async -> Int { await withTaskGroup(of: Int.self) { group in for _ in 0..<5 { group.addTask { try? await Task.sleep(for: .seconds(Int.random(in: 1...3))) return Int.random(in: 1...100) } } var sum = 0 for await value in group { sum += value } return sum } } let result = await randomSum() print(result)