When Swift Concurrency burst onto our screens five years ago, it dangled AsyncImage before our dazzled eyes — see how easy it is now, to download and display an image! It was quick and fun to use … in a prototype or sample app, not in a production app that needed image caching for efficiency and performance. Well, Xcode 27 fills that gap, bringing AsyncImage caching into your life!
Getting Started
Note: You need Xcode 27 beta to use AsyncImage caching.
Use the Download Materials button at the top or bottom of this article to download the starter project.
TheMet is an app that searches The Metropolitan Museum of Art, New York for museum objects matching the user’s query term. It’s adapted from the sample project in Section III of SwiftUI Apprentice.
TheMet app has a repository TheMetStore and a networking service TheMetService. ContentView displays a color-coded list of museum objects. Pale blue items are in the public domain, and you can download images. Red items are not in the public domain, and you must navigate to the museum’s web site to view images.
Tapping an object that’s in the public domain navigates to ObjectView, which displays information about the object and uses AsyncImage as a simple way to fetch an image of the object. Tapping a red item navigates to a SafariView of the object’s web page on the Metropolitan Museum’s web site.
Caching Images
AsyncImage is useful for prototyping an app, but it’s inefficient to fetch an image every time ObjectView loads. Your app’s performance is better if it caches images.
Some of the Kodeco iOS bootcamps used TheMet to teach lists, navigation and networking. An Above-and-Beyond homework exercise asked the students to modify it to cache the images. At the time, this required adding an image data property to Object and replacing AsyncImage with a URLSession task to download and store the image data. Caching code could be adapted from the EmojiArt project in Modern Concurrency. It was a lot of work!
Xcode 27 adds caching to AsyncImage!
AsyncImage caches downloaded image data following the transport protocol. The system creates the cache with a default URLSessionConfiguration. To change the cache policy, specify the change in URLRequest, and pass it to init(request:scale:transaction:content:). To customize the download process in a specific view hierarchy, use asyncImageURLSession(_:) to specify a URLSession. AsyncImage uses this session to perform data tasks when downloading the image data.
In this tutorial, I interpret “default URLSessionConfiguration” to mean that AsyncImage uses URLSession.shared as its default URLSession. As you’ll see, when AsyncImage is using the default session, the size of URLCache.shared increases whenever AsyncImage downloads a new image. You’ll also learn how to change the cache policy and how to specify a custom URLSession.
Caching in Action
The quickest way to see AsyncImage caching in action is to compare what it does in Xcode 26 with what happens in Xcode 27. If you’re reading this after Xcode 27’s release, and don’t have Xcode 26 anymore, skip to the next section, where you’ll set the cachePolicy of a URLRequest.
If you’re reading this while Xcode 27 is still in beta, open TheMet in Xcode 26, then open ContentView.swift and load its preview. When the rhino list loads, tap one of the pale blue items to load its ObjectView.
After the image loads, navigate back to the list. Now, turn off wifi on your Mac, then tap the same blue item — no image!
Xcode 26’s AsyncImage doesn’t have caching, so ObjectView tries — and fails — to download the image again.
Quit Xcode 26, turn wifi back on, then open TheMet in Xcode 27. In the ContentView preview canvas, tap a different pale blue item, wait for the image to load in ObjectView, then tap back to the list.
Turn off wifi, then tap the same item — AsyncImage retrieves the image from its cache!
Setting Cache Policy
In ContentView, pin its preview:
Now, open ObjectView.swift and locate the AsyncImage(url:) call.
AsyncImage uses the shared URLSession instance, which uses the default URLSessionConfiguration. The default cachePolicy is useProtocolCachePolicy, whose key behavior is:
if the cached response does not indicate that it must be revalidated every time, and if the cached response is not stale (past its expiration date), the URL loading system returns the cached response.
Xcode documentation provides this handy flow chart:
The available cache policies are enumerated in NSURLRequest.CachePolicy. They allow you to ignore local and/or remote cache data or use possibly-expired cache data:
-
reloadIgnoringLocalCacheData: The URL load should be loaded only from the originating source. -
reloadIgnoringLocalAndRemoteCacheData: Ignore local cache data, and instruct proxies and other intermediates to disregard their caches so far as the protocol allows. -
returnCacheDataElseLoad: Use existing cache data, regardless of age or expiration date, loading from originating source only if there is no cached data. -
returnCacheDataDontLoad: Use existing cache data, regardless of age or expiration date, and fail if no cached data is available. -
reloadRevalidatingCacheData: Use cache data if the origin source can validate it; otherwise, load from the origin.
You can change cachePolicy in a URLRequest. Just above the AsyncImage(url:) call is a TODO:
TODO: Create request to set its cachePolicy
Replace this with the following code:
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
Then, in the next TODO, replace the url initializer:
AsyncImage(url: url) { image in
with the URLRequest initializer:
AsyncImage(request: request) { image in
This initializer is new in Xcode 27.
Now, in the ContentView preview, go through the same routine: load an image in ObjectView, return to the list, turn off wifi, then load the same ObjectView. Because you’re now ignoring local cache data, there’s no image.
Change cachePolicy to useProtocolCachePolicy and see for yourself that AsyncImage retrieves the cached image.
Remember to turn wifi back on.
Displaying Cache Size
You can get or set on-disk and in-memory cache properties:
- currentDiskUsage
- diskCapacity
- currentMemoryUsage
- memoryCapacity
Scroll down in ObjectView to the last Text view. Just below this is another TODO:
Display current disk usage
Replace the TODO with the following code:
Text("Shared Cache (KB): " +
String((URLCache.shared.currentDiskUsage / 1024)))
Yes! URLCache has a class variable shared — much shorter than URLSession.shared.configuration.urlCache!
Now, when you tap a list item, you see how much is in the shared cache:
Your number might be different, depending on how many times you’ve loaded ObjectView.
