SwiftUI Async Image tutorial

SwiftUI Async Image

In this SwiftUI tutorial, we will learn how to load and display an image asynchronously from the Internet. Note that the SwiftUI Async Image view uses the shared URL Session instance to load an image from a specified URL and then display it on the screen.

Until the remote image loads, the view displays either a standard or a custom-built placeholder.

To gain more control over the loading process, we can use the initializer, which takes a content closure that receives an Async Image Phase to indicate the state of the loading operation.

By doing that, we can control what to display on the screen. In a nutshell, we can return a view that's appropriate for the current phase, such as success, failure, or empty phase. Alright! Without further ado, let's create a new project in Xcode and start coding.

Async Image Basics

The simplest way to use Async Image is by specifying the image URL. To do that, first, we need to create a new property that will store the web address of the image in a string.

private let imageURL: String = "https://credo.academy/credo-academy@3x.png"

After that, we will replace the default Text view with the simplest form of Async Image like this:

// MARK: - 1. BASIC

AsyncImage(url: URL(string: imageURL))

With this short and quite straightforward piece of code, we can download and display a remote image. The first thing you can notice on the Preview is a gray placeholder.

The reason behind it is since the URL is optional. Therefore, the Async Image will show a default gray placeholder if the URL string is invalid.

And if the image can't be loaded for some reason, for example, if the user is offline or the image doesn't exist, then the system will continue showing the same placeholder image.

That's said, let's see what will happen when we start the Live Preview. There it goes!

Async Image Scale

Once the remote image is completely downloaded, Async Image displays this image in its original size. You know, I uploaded this huge image to the server for you on purpose so I can show you how to scale it down.

To make it happen, first comment out this line of code and start a new section by entering this code.

// MARK: - 2. SCALE

AsyncImage(url: URL(string: imageURL), scale: 3.0)

And, that's it! We can see the whole image and not only part of it. If we want to make the image smaller or larger, then we need to pass a scaling value to the scale parameter like this.

The default value is always 1.0. The greater the value is, the smaller the image is. That's why the 2.0 and the 3.0 will scale down the image.

Conversely, a value less than one will make the image bigger.

Let's try to change the scale value from three-point zero to two-point zero, and let's see what will happen, shall we? As you can see, the image scaled up a little bit.

Now let's check out the remote image with the default one point zero before scaling back to the ideal size. From now, working with different image sizes shouldn't be hard since you know how this works.

Async Image Placeholder

Alright! Now let's jump to the Async Image's next feature, shall we? Async Image provides another constructor for developers if we need further customization.

Comment out the previous line and enter the following code.


AsyncImage(url: URL(string: imageURL)) { image in
} placeholder: {
Image(systemName: "photo.circle.fill")
  .frame(maxWidth: 128)

As you can see, we have just added a tiny icon as a placeholder for this Async Image. Now with this initializer, we can also use the content and the placeholder parameters to manipulate the loaded image.

For example, we can add some modifiers to make either the remote image or the placeholder resizable. Let's do it right now.

And? There is a customized placeholder. What do you think about it? It's pretty obvious that our own implementation for the placeholder is much better than a blank gray sheet.

However, we must keep in mind one important thing about these modifiers. We can not apply image-specific modifiers, like Resizable, directly to an Async Image.

Instead, we must apply them to the Image instance that our content or placeholder closures get when defining the view's appearance. So far, we covered how to create a basic Async Image, and then we scaled it appropriately.

Finally, we learned how to add a custom placeholder for it. We discovered a lot, but not everything, since there is still one more thing that we should learn about the Async Image.

Image Extension

But before we continue developing this mini-application, we should hold on a second and look at our recent code. As you can notice, we added many modifiers for the image, and since we will create a new Async Image, therefore we need to repeat this code which is not so fun after a while, to be honest.

But not only that, there are already some duplicated modifiers in this code as well. It would much better if we could optimize these modifiers somehow. And that's what we will do by creating a reusable Image extension. First of all, select the image's modifiers and cut them out to the clipboard.

After that, we need to navigate the cursor outside the scope of the Content View. Scroll to the top and enter the following code!

extension Image {
func imageModifier() -> some View {
func iconModifier() -> some View {
    .frame(maxWidth: 128)

Here we need to paste the modifiers from the clipboard, as I show you. So far, so good! Now we will continue with creating another function for the placeholder's modifiers.

Please notice that we have just embedded the previous modifiers into this new one.

How cool is the Swift language?

After all this, we need to cut and paste the remaining modifiers in the placeholder into this image extension. So let's do it! Now it's time to add all the necessary image modifiers to the image and placeholder again.

Go to the image and enter:


Then jump to the placeholder and add this modifier to it.

Image(systemName: "photo.circle.fill").iconModifier()

Nothing changed in the Preview, which means that our code works as it should do. However, our code is much cleaner now, not to mention that we can use the image extension over and over again.

Async Image Phase

Now the fun part begins! Select all code in the third section and comment out as I show you.

Then let's create a new section for the Async Image Phase.

// MARK: - 4. PHASE

AsyncImage(url: URL(string: imageURL)) { phase in
// SUCCESS: The image successfully loaded.
// FAILURE: The image failed to load with an error.
// EMPTY: No image is loaded.

if let image = phase.image {
} else if phase.error != nil {
  Image(systemName: "ant.circle.fill").iconModifier()
} else {
  Image(systemName: "photo.circle.fill").iconModifier()

To gain more control over the loading process, we need to use this new initializer, which takes a content closure that receives an Async Image Phase to indicate the state of the loading operation.

Also, we should take into consideration that this will return a view that's appropriate for the current phase.

Now, let's see in action how we can work with these Async Image Phases, shall we?

And these are the Async Image Phases:

1. Success! The image was successfully loaded.
2. Failure! The image failed to load with an error.
3. And finally, the Empty phase, when no image is loaded.

As you can see, the Async Image view provides another constructor if we want to take more control of the asynchronous operation. That's being said, we can display different images in each distinguished phase, such as the success phase, then the failure phase, and finally the empty stage.

But enough with the talk, and let's see how this works by using the Live Preview. After starting the live Preview, we can see the remote image replacing the placeholder image.

So far, nothing new here.

But this will change once we modify the image's URL. So that way, the app could not load the remote image anymore. For example, we can remove a letter at the end of the string, and voila!

By refreshing the live Preview, the URL session's new response will be a failure. That's said, this icon with an ant will represent the failed content on the screen, replacing the placeholder image.

How fantastic is that?

Of course, we can play with it for a while until we check out how all three Async Image Phases work. By the way, it is not the only way to use Async Image Phases. In the rest of the SwiftUI tutorial, I will show you an alternative mode of doing that.

However, to make this more interesting, we will add some animation to this Async Image and make it really cool!

Async Image Animation

The process is the same. Select the fourth section code and comment out as I show you. After that, let's create the final section and make the best of the SwiftUI's Async Image. Enter the following code!

As you can see, we used the Switch statement instead of using the traditional If, Else conditional.

The reason behind this is the following:

The Async Image Phase is actually an enumeration that keeps track of the current phase of the download operation. Knowing this fact, we can provide a detailed implementation for each of the phases, including the empty, failure, and success phases.

This code is so elegant, am I right? Just one more thing!

As you can notice, there is a gently warning that the compiler gives us.

You know, this code works totally fine since we covered all available phases. Yet, we should make our code more bullet-proof and less error-prone.

Click on the warning sign, and read what the compiler is saying about it. There it goes!

Basically, software engineers at Apple could introduce more phases for the SwiftUI's Async Image in the future, and that's why we can avoid potential app crashes by providing a default value.

By clicking on the Fix button, Xcode will help us with inserting the missing code. Do you see the new code at the end?

Just replace the fatal error with a simple Progress View. That's it! Now let's start the Live Preview and test the application.

Guess what?

The warning is gone, and our code works as it is supposed to do. The only thing that is still missing the animation feature that I promised you before.


AsyncImage(url: URL(string: imageURL), transaction: Transaction(animation: .spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0.25))) { phase in
phase {
case .success(let image):
case .failure(_):
  Image(systemName: "ant.circle.fill").iconModifier()
case .empty:
  Image(systemName: "photo.circle.fill").iconModifier()
@unknown default:

To animate the remote image, we need to specify an optional transaction parameter for the Async Image. Navigate the cursor to the URL parameter and enter the following code snippet!

With this transaction, we defined what kind of animation we want to run each time when the image downloads from the Internet. The final information that we still need to provide to work this animation is some type of transition.

And o gosh!

There are plenty of transitions that we can choose from. In the rest of the tutorial, we will have some fun playing with some of the available transitions.

Does it sound good? I hope so! So let's do it!

Go to the image and add this new modifier to it.

.transition(.move(edge: .bottom))

After that, let's refresh the Live Preview and see what's happening.


What a surprise that animation with Async Image does not work in the Preview. Don't worry too much about it, since testing animation is always better on an actual device, even we could do it in the Simulator.

With that said, build and run the application in the Simulator as I show you. After the initial launch, we can see the picture icon in the empty phase, which works like a placeholder.

When the remote image is downloaded, then the chosen animation starts. Now let's try out a different transition, shall we? Jump back to Xcode.

First of all, comment out the move transition, then let's add a different modifier to the image. Of course, we need to build the app and test the slide animation in the Simulator, so let's do it again!

Depending on the language writing direction, the remote image is sliding into the middle of the screen from the left or the opposite side.

What an amazing animation that is!

Let's test it again! But you know what?

Let me show you my favorite transition.

Jump back to Xcode and replace the previous image modifier with this one.

If you test the app on a real device or in the Simulator, then you should notice how great the spring transition fits this scale effect. I hope that you like the final result so much as I do. With building this mini-application, we covered all features of SwiftUI's Async Image.

From iOS 15, no third-party plug-in is needed if we would like to load remote image assets from the Internet.

Wrap Up

Just to recap the main features of this dedicated Async Image view. To load an image from the Internet is as simple as providing an optional URL for the Async Image. Next!

By default, the image is assumed to have a scale of one, meaning it is designed for non-retina screens. We can change it by modifying the value of the Scale parameter.

Another thing is that we can not apply image-specific modifiers directly to an Async Image. Instead, we must apply them to the image instance.

It's worth knowing that we can specify a custom placeholder and replace the default gray view.

Finally, we can also utilize the enum of the Async Image Phase in the content closure.

With this Async Image Phase, we can indicate the state of the loading operation.

And by doing that, our program returns a view that is appropriate for each phase, such as empty phase, failure phase, or success phase.

In this SwiftUI tutorial, we covered many code examples of how to use the Async Image properly so you can implement this knowledge into your development workflow.

I hope that you enjoyed this lecture and you are eager to start the next one.

Until then, happy coding!

Table of Contents

In each section we will cover something new, something intersting about SwitftUI 3 Async Image view.

 Watch the Async Image Tutorial