In this fork https://github.com/MahiAlJawad/CachedAsyncImage-SwiftUI error handling has been added, which was missing in the original.
SwiftUI provides a clean AsyncImage facility to download an asynchronous image and show a placeholder (e.g. ProgressView/Spinner) during image download. However the SwiftUI library does not cache the image, which results in redownloading the image each time. This repo provides a cleaner solution written over the AsyncImage library to support the caching facility.
I have originally answered this solution in a StackOverflow question for image caching: https://stackoverflow.com/a/77956449/18823687 and StackOverflow community found the answer useful. So I have created a repo to share this library in the iOS developer community.
Just copy-paste the following library code in a Swift file
//
// CachedAsyncImage.swift
// inSkate
//
// Created by Dzmitry Sotnikov on 07.03.2026.
//
import SwiftUI
@MainActor
struct AsyncCachedImage<ImageView: View, PlaceholderView: View, ErrorView: View>: View {
// Input dependencies
var url: URL?
@ViewBuilder var content: (Image?) -> ImageView
@ViewBuilder var placeholder: () -> PlaceholderView
@ViewBuilder var errorView: () -> ErrorView
// Downloaded image
@State var image: UIImage?
// Error if occured
@State var error: Error?
init(
url: URL?,
@ViewBuilder content: @escaping (Image?) -> ImageView,
@ViewBuilder placeholder: @escaping () -> PlaceholderView,
@ViewBuilder errorView: @escaping () -> ErrorView
) {
self.url = url
self.content = content
self.placeholder = placeholder
self.errorView = errorView
}
var body: some View {
VStack {
if let uiImage = image {
content(Image(uiImage: uiImage))
} else if error != nil {
errorView()
} else {
placeholder()
.onAppear {
Task {
image = await downloadPhoto()
}
}
}
}
}
// Downloads if the image is not cached already
// Otherwise returns from the cache
private func downloadPhoto() async -> UIImage? {
do {
guard let url else {
self.error = URLError(.badURL)
return nil
}
// Check if the image is cached already
if
let cachedResponse = URLCache.shared.cachedResponse(for: .init(url: url)),
let imageFromCache = UIImage(data: cachedResponse.data)
{
return imageFromCache
} else {
let (data, response) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
self.error = URLError(.cannotDecodeContentData)
return nil
}
// Save returned image data into the cache
URLCache.shared.storeCachedResponse(.init(response: response, data: data), for: .init(url: url))
return image
}
} catch {
PRINT_DEBUG("Error downloading: \(error)")
self.error = error
return nil
}
}
}Same as original SwiftUI library AsyncImage
AsyncCachedImage(url: photoURL) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
ProgressView() // Shows loading indicator until the image is downloaded
} errorView: {
ErrorView() // Your error reaction view
}