The Case Against AsyncImage

Alex Persian
5 min readFeb 19, 2024

--

Reasons why AsyncImage doesn’t work for non-standard usages.

Intro

AsyncImage is a really slick tool that SwiftUI has on offer. It allows for extremely easy remote image loading, and it includes tools for animation as well as working with loading state changes.

// This is all that's required to load a remote image, including a loading
// spinner as a placeholder, and animating the transition.
AsyncImage(
url: URL(string: imageURL),
transaction: Transaction(animation: .default),
content: { phase in
if let image = phase.image { image }
else { ProgressView() }
}
)

Once you start to need more custom functionality or behaviors outside of the simple fire-and-forget asset loading though, it begins to quickly show its limitations. In this short writeup I will go over some of those limitations I’ve encountered while using this in my own projects.

Caching

Under the hood AsyncImage uses the shared URLSession for loading operations. This means that it also uses the default shared URLCache. This is fine for more use cases when working with moderately sized images without network load restrictions. You can adjust that shared cache to increase the size if necessary.

URLCache.shared.memoryCapacity = 10 * 1024 * 1024   // 10MB
URLCache.shared.diskCapacity = 20 * 1024 * 1024 // 20MB

This is the extent of your control over caching though. Because the configuration for the shared URLSession is immutable it means that the cache policy cannot be changed from useProtocolCachePolicy, which may not always be the desired behavior. Such as if you want to prioritize loading from the cache or supporting offline-mode by loading from cache only.

// Even though this does not throw an error it does not change the config
URLSession.shared.configuration.requestCachePolicy = .returnCacheDataElseLoad

There are normally two approaches to this problem:

  1. Use our own URLSession object with a configuration that supports our preferred caching strategy.
  2. Build our own image caching solution separate from URLCache.

(1) Because AsyncImage does not expose or allow customization of its loading process, the first approach is not possible as we can’t tell AsyncImage to not use the shared URLSession object.

(2) Because AsyncImage, since its a SwiftUI view, does not expose or provide an easy way to access its image property or allow for short-circuiting the load step, it means that using our own image caching solution would not be viable.

For these reasons if non-standard caching behavior is desired then you are better off using Image and handling the async loading yourself. Or you can use one of the AsyncImage alternatives listed below.

Decoding

Another one of the limitations with using AsyncImage is that the image decoding step after data fetch is a black box.

For normal usages this is fine; loading a single image this way won’t create much stress. But if you are loading a ton of images, such as within a list, and for whatever reason don’t have access to properly sized thumbnail assets, then using AsyncImage to load the images will cause significant animation hitching during scroll.

Example of scroll hitches while loading large images

In the UIKit world you can leverage the powerful UIImage APIs such as prepareForDisplay in combination with UICollectionView prefetching to ensure smooth scrolling performance even with unoptimized image assets. This tactic was covered in excellent detail by this WWDC’21 talk, which I highly recommend watching.

AsyncImage does not provide a way to intercept this logic as you can’t override its loading pipeline, and you can only initialize one using a URL. This is great for ease of use, but does hinder our ability to optimize the loading processes. The alternative would be to use an Image view and handle the asynchronous loading and decoding yourself.

Saving

In UIKit when you want to save an image from a UIImageView you can simply access it and then save that image asset. In the SwiftUI realm it’s not that simple though because the view no longer holds onto that state. You can’t ask an AsyncImage view object what its image asset is.

// UIKit
if let image = someImageView.image {
// save that image
}

// SwiftUI
let imageView = AsyncImage(url: someURL)
imageView.image // Error: Value of type 'AsyncImage' has no member 'image'

Instead Apple provides a way to render a SwiftUI view itself into an image buffer, and then use that to save an image file. This is handled by the ImageRenderer API.

// Must be done on the @MainActor
let renderer = ImageRenderer(content: imageView)
if let image = renderer.uiImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}

Trying this with an AsyncImage instance immediately shows the first issue; the rendered out image is empty. On top of that, if you use a loading view such as ProgressView in the phase block then the renderer will error out and save a red & yellow warning symbol as the image. This is likely due to views like ProgressView (aka CircularUIKitProgressView) being wrappers around existing UIKit views which ImageRenderer won’t work with.

Empty image saved (left), error image saved (right)

An alternative to ImageRenderer is available where you wrap the SwiftUI view in a UIHostingController, bridging it back to the UIKit space, and then that UIKit-backed view gets rendered out and saved (ref. Hacking with Swift for example code). This is an approach that can handle AsyncImage, even with a ProgressView, but it has its own caveats.

Examples of views saved incorrectly using UIHostingController. Empty space highlighted in red.

You must ensure you’re rendering at the correct resolution & scale, and that the rendered view is not being cut off, skewed, or not filling the full size within the UIHostingController. Otherwise the saved asset will not be accurate. These extra requirements can lead to more complicated code and error prone results.

Conclusion

AsyncImage is a fantastic API provided by Apple to support super easy and seamless image loading in the majority of use-cases. Hopefully this post has given you some insight into the edge cases where it might not be the best choice for ensuring smooth user experiences though.

If you found this helpful, or if you found errors in this post, please let me know in the comments! I’ve also included a handful of alternatives to AsyncImage that I recommend checking out below. Thanks for reading!

Alternatives

  • Basic RemoteImageLoader. This is a great foundation and it can be easily extended to support more robust loading, caching, etc.
  • CachedAsyncImage. Great alternative if you want a drop-in replacement for AsyncImage that supports better caching.
  • AsyncPhoto. Good alternative that uses the prepareForDisplay API to handle large image assets better.
  • Nuke. This is a fully fledged library that can be used in-place of AsyncImage with a focus on performance.

--

--