I was helping my friend Freddy with his project called BritMap — an interactive map of the city of York, UK. It lets you explore the secrets, buildings, and historic figures around you.
SwiftUI. Mapbox SDK. Easy peasy, lemon squeezy.
We received a lot of positive feedback. But some of my friends complained that it was crashing on their old iPhone 12.
It wasn’t hard to spot the issue — even without a bug report:
AsyncImage is great, but it comes with some trade-offs:
-
It downloads the full-size image, even if you’re only showing it as a thumbnail
-
It doesn’t cache images by default, meaning every scroll or reload hits the network again
-
And if you’re loading a bunch of images at once (like we were), performance takes a lot
I’ve heard a lot of complaints about how SwiftUI handles lists. I faced it myself a few months ago with another one of my pet projects (don’t worry, I’m not asking you to download it — just know it only had six images!). Each image was around 2–3MB, and even my design colleague noticed the scroll glitch. Insane!
But in the Britmap we are getting images from URLs. So we should go back to our old good CoreImages to resize them.
private func resizeIfNeeded(data: Data) -> UIImage? {
guard let targetSize else {
return UIImage(data: data)
}
let scale = UIScreen.main.scale
let options: [CFString: Any] = [
kCGImageSourceShouldCache : false,
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize : max(targetSize.width, targetSize.height) * scale
]
guard
let src = CGImageSourceCreateWithData(data as CFData, nil),
let cg = CGImageSourceCreateThumbnailAtIndex(src, 0, options as CFDictionary)
else { return nil }
return UIImage(cgImage: cg)
}
After the fix:
The issue has been fixed. All crashes are gone.
You can use (or modify, or suggest improvement) by using the following SPM.
.package(url: "https://github.com/AnthonyBY/AsyncResizableImage.git", from: "1.0.0")
I will be happy to hear any feedback.
TL;DR:
So, If you need a simple replacement for SwiftUI’s built-in AsyncImage
, you can just copy this file:
Import SwiftUI
@MainActor
struct AsyncResizableImage<ImageView: View, PlaceholderView: View>: View {
var url: URL?
var targetSize: CGSize?
@ViewBuilder var content: (Image) -> ImageView
@ViewBuilder var placeholder: () -> PlaceholderView
@State var image: UIImage? = nil
init(
url: URL?,
targetSize: CGSize? = nil,
@ViewBuilder content: @escaping (Image) -> ImageView,
@ViewBuilder placeholder: @escaping () -> PlaceholderView
) {
self.url = url
self.targetSize = targetSize
self.content = content
self.placeholder = placeholder
}
var body: some View {
VStack {
if let uiImage = image {
content(Image(uiImage: uiImage))
} else {
placeholder()
.onAppear {
Task {
image = await downloadImage()
}
}
}
}
}
private func downloadImage() async -> UIImage? {
guard let url else { return nil }
if let cached = URLCache.shared.cachedResponse(for: .init(url: url)) {
return resizeIfNeeded(data: cached.data)
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
URLCache.shared.storeCachedResponse(.init(response: response, data: data), for: .init(url: url))
return resizeIfNeeded(data: data)
} catch {
print("Error downloading: \(error)")
return nil
}
}
private func resizeIfNeeded(data: Data) -> UIImage? {
guard let targetSize else {
return UIImage(data: data)
}
let scale = UIScreen.main.scale
let options: [CFString: Any] = [
kCGImageSourceShouldCache : false,
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize : max(targetSize.width, targetSize.height) * scale
]
guard
let src = CGImageSourceCreateWithData(data as CFData, nil),
let cg = CGImageSourceCreateThumbnailAtIndex(src, 0, options as CFDictionary)
else { return nil }
return UIImage(cgImage: cg)
}
}
Enjoy