Categories
Uncategorized

How to use AsyncImage in SwiftUI

In this tutorial, you’ll learn everything you need to know about the new AsyncImage in SwiftUI. You learn how to fetch Image data asynchronously in a safe and easy way and how to provide a great user experience by utilizing placeholders and covering errors.

This is how our AsyncImage will look like:

Why and when should we use AsyncImage in SwiftUI ❓

When we build SwiftUI apps, we often face the situation that at some point we need to download images from web servers. For example, if we want to display a photo feed or we want to use images from a specific database.

Until now, we had to build elaborate structures for this and download the required image data using URLSession & Co. while using completion handlers to display it inside an Image view.

However, since iOS 15, SwiftUI provides an AsyncImage view designed to do this job for us and load images from the web asynchronously. Asynchronous means – roughly speaking – that the task of downloading the image is completed in the background, i.e. we don’t need to wait until the task finishes. This greatly simplifies the image loading process in SwiftUI.

Basic structure of an AsyncImage 🌌

AsyncImage comes with a few initializers, the simplest of which takes an optional URL as the parameter. This means you don’t have to force unwrap the URL, and AsyncImage will either display the image if loading was successful or display the default placeholder, which is a gray color. 

To begin, place an instance of AsyncImage in the project you are working with.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: )
    }
}

Next, we provide it with the URL for an image off the web. I’m using this image for my example:

Sample Image Data for SwiftUI AsyncImage

Let’s copy & paste the URL of this image into our AsyncImage.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"))
    }
}

Let’s try fetching the image from the web using the AsyncImage instance by starting a Live preview.

You may have noticed that by default, AsyncImage takes up all the available space. You may want to control how much space it takes up by adding a .frame modifier.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"))
            .frame(height: 240)
    }
}

However, this does not work as you’d expect: The placeholder displays the correct size set by the frame, but when the image loads, it displays in its natural size. This happens because AsyncImage does not know the size of the image it is loading. Thus it after loading, the image may be smaller or bigger than the frame allocated, and will be displayed as such. 

AsyncImage in SwiftUI Framing

Modifying AsyncImages properly and using Placeholder views 🔁

To control how the image is displayed after it is loaded, AsyncImage provides initializers that give access to the loaded image. You can use the init(url:scale:content:placeholder) initializer which provides a content closure that takes an Image instance and returns a View. Thus we can use it to attach modifiers to the resulting image. For example, we could attach a .resizable or a .aspectRatio modifier to size the image properly.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"), scale: 2) { image in
            image
              .resizable()
              .aspectRatio(contentMode: .fill)
        } placeholder: {
            Color.gray
        }
            .frame(height: 240)
    }
}

The placeholder parameter allows us to specify a custom View that displays as the placeholder while the image is loading. For example, we can use a simple spinning activity indicator as the placeholder

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"), scale: 2) { image in
            image
              .resizable()
              .aspectRatio(contentMode: .fill)
        } placeholder: {
            ProgressView()
                .progressViewStyle(.circular)
        }
            .frame(height: 240)
    }
}

More control using AsyncImagePhase 💡

If there was an error loading the image, AsyncImage simply displays the placeholder you provided. However, you may want to control what is displayed in case there was an error. To handle this, AsyncImage provides another initializer, init(url:scale:transaction:content). Here, the content parameter is not the image itself, rather it is an AsyncImagePhase instance that gives you access to the loading state of the image which can handle by using a switch statement.

By handling the different cases of the phase we can react accordingly. For example, you could add a red Text to display when there was an error loading the image.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg")) { phase in
            switch phase {
            case .empty:
                ProgressView()
                    .progressViewStyle(.circular)
            case .success(let image):
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            case .failure:
                Text("Failed fetching image. Make sure to check your data connection and try again.")
                    .foregroundColor(.red)
            }
        }
            .frame(height: 240)
    }
}

We can also add the @Unknown case to handle phase cases that may be added in future versions.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"), transaction: .init(animation: .spring(response: 1.6))) { phase in
            switch phase {
            case .empty:
                ProgressView()
                    .progressViewStyle(.circular)
            case .success(let image):
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            case .failure:
                Text("Failed fetching image. Make sure to check your data connection and try again.")
                    .foregroundColor(.red)
            @unknown default:
                Text("Unknown error. Please try again.")
                    .foregroundColor(.red)
            }
        }
            .frame(height: 240)
    }
}

Using the transaction modifier we can define a custom animation of how our image should appear once it’s fetched.

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://blckbirds.com/wp-content/uploads/2021/10/pexels-kammeran-gonzalezkeola-6128227-2.jpg"), transaction: .init(animation: .spring())) { phase in
            //...
        }
            .frame(height: 240)
    }
}

Let’s run our app in the standard simulator to check if our animation works as expected:

Conclusion 🎉

That’s it. You learned everything you need to know about AsyncImage in SwiftUI. You see that with AsyncImage views it’s very easy to fetch and display images from the web.

Did you like the tutorial and want to learn more about developing iOS apps with SwiftUI? Then make sure to check out our Interactive Mastering SwiftUI Book!