At the time of writing this post, WWDC21 is still in progress. And as it turns out, this year’s announcements reveal really great and exciting news for developers. Among them, SwiftUI seems that has been equipped well with new APIs, views and tools that will undoubtedly make building apps with it an even more easier and fun task.
A new and quite interesting view introduced in this, third release of SwiftUI, is the AsyncImage. As the name makes it pretty obvious, this view displays images after having fetched them from a remote URL. Doing so has been traditionally a manual task, but now AsyncImage does all the work behind the scenes until the image has been presented in the view. The AsyncImage API is simple, yet flexible enough; it provides options to display a placeholder image while waiting for the remote one, deal with potential errors, show the downloaded image animated, and of course, style the image as we like using view modifiers.
There is only one downside, and that is that fetched images are not cached for future use. AsyncImage view downloads the remote image whenever it’s about to be displayed. That’s okay for images that will be shown to users just a few times. But for remote images that an app presents often, it’s not recommended to always fetch them in real time. A manual implementation to fetch and cache images is still necessary.
Hands-on to the AsyncImage
Time to play around a bit with the AsyncImage and see what it does. The simplest way to use it is like so:
1 2 3 |
AsyncImage(url: url) |
The url
argument is a URL object pointing to a remote image:
1 2 3 |
let url = URL(string: “https://images.pexels.com/photos/8016369/pexels-photo-8016369.jpeg”) |
If you add the above to a SwiftUI view and run it in a simulator, or just live preview it, you’ll see the image being displayed after a few moments. However, that’s not so much user friendly; a default gray colored box is shown temporarily as a placeholder. We need to indicate to our users that an asynchronous operation is running possibly with a progress view and some message, or display a more proper placeholder image until the remote one has been fetched.
Let’s focus on using a placeholder image first. In order to achieve that, there’s another initializer to use: init(url:scale:content:placeholder:)
.
The first argument is the URL to the image, and the second is the scale to use for the image. The default scale value is 1. The other two arguments are closures, which I’ll explain more about after having seen that new initializer in action:
1 2 3 4 5 6 7 8 9 10 11 |
AsyncImage(url: url) { image in image .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 15)) .padding() } placeholder: { placeholderImage() } |
I’m not providing a value for the image scale here; I skip it, so the default one will be used instead.
Let’s talk about the two closures now. The argument of the first closure is a SwiftUI Image view containing the image after it has been downloaded. We present it in the closure’s body, and as you can notice, we can use any view modifier that will style the image appropriately.
The second closure is the place to add an Image view with a placeholder image. This one is going to be displayed for as long as we wait for the remote one to be fetched. It will keep being displayed in case the actual image cannot be loaded for some reason. In this example I’m calling the placeholderImage()
method, which is the next one:
1 2 3 4 5 6 7 8 9 10 11 |
@ViewBuilder func placeholderImage() -> some View { Image(systemName: “photo”) .renderingMode(.template) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 150, height: 150) .foregroundColor(.gray) } |
The placeholderImage()
method returns a SwiftUI view, therefore it’s marked with the @ViewBuilder
attribute. As a placeholder image I’m using an SF Symbol.
Credits: Photo by Lucy from Pexels.
AsyncImage and the AsyncImagePhase
AsyncImage has another initializer that allows to get the image load results in phases: init(url:scale:transaction:content:)
.
See the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
AsyncImage(url: url) { phase in switch phase { case .success(let image): image .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 15)) .padding() case .failure(let error): Text(error.localizedDescription) case .empty: waitView() @unknown default: EmptyView() } } |
Here I’m supplying only the URL and the content
argument, which is a closure. The closure’s argument is an AsyncImagePhase value, where AsyncImagePhase is an enum with cases that represent certain load phases.
In fact, you can see all available cases of the AsyncImagePhase in the switch
statement right above. The first one is the case where the image has been successfully fetched. The associated value of the case is a SwiftUI Image view, which we can decorate using any view modifiers necessary.
The second case contains the potential error that has might occurred. In this example I’m just presenting the error description in a Text view.
The last case called .empty
represents the state of the AsyncImage view when there is not a loaded image yet. Here, we can add either a placeholder view, a progress view, or anything else fitting to the app. In the above example I’m calling the waitView()
method, which is the following:
1 2 3 4 5 6 7 8 9 10 11 |
@ViewBuilder func waitView() -> some View { VStack { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .indigo)) Text(“Fetching image…”) } } |
Its purpose is to present a progress view along with a text.
See that there is also an @unknown default
case in the switch
statement; that’s necessary to handle any new values that might be added to the AsyncImagePhase enum in the future. An EmptyView is the best candidate to handle such a case.
The above AsyncImage implementation results to this:
In general, notice that the above way to initialize an AsyncImage view provides greater control over the view’s results. In particular, we can actually detect when an error has occurred with this one and therefore display a different view; something that we can’t do using the placeholder initializer met in the previous part.
But that’s not the only benefit of this initializer. Through the transaction
argument we can pass an animation that will be put in motion when the phase changes. Right next you can see the same example as before, only an animation instance is provided to the transaction
argument this time:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
AsyncImage(url: url, transaction: Transaction(animation: .easeInOut(duration: 2.5)) ) { phase in switch phase { case .success(let image): image .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 15)) .padding() case .failure(let error): Text(error.localizedDescription) case .empty: waitView() @unknown default: EmptyView() } } |
Summary
This is pretty much everything about using the AsyncImage view in order to load and display remote images. There are two things I didn’t mention in the introduction; the first one is that AsyncImage uses the shared URLSession instance to fetch an image from the given URL. The second, and probably the frustrating news, is that AsyncImage is available in iOS 15 and above. Nevertheless, it’s a great new view that will be useful beyond any doubt, and an amazing new addition to the SwiftUI arsenal. Thank you for reading!
You can find this post published on Medium too!