SwiftUI - Using AsyncImage API in iOS15

SwiftUI - Using AsyncImage API in iOS15

Apple introduced an AsyncImage API in iOS15. This API solves the long-standing image download problem for iOS developers without using any third-party image downloading libraries.

Asynchronously downloading remote images from URL has been a long pain point and having to add third-party frameworks only adds to this pain. Let's see how we can asynchronously download the remote images from a given URL using a native way.

Meet AsyncImage in iOS 15

AsyncImage is a view that asynchronously loads and displays an image. It uses a shared URLSession to download the image and then display it in the app. All we have to do is to provide an URL of the remote image and (optionally) the size of the AsyncImage view and that's all.


let urlString = "https://picsum.photos/200/300"
AsyncImage(url: URL(string: urlString))


While the image is downloading, the view will show a standard placeholder view and replace it with the image once the download completes.

0:00
/

Alternatively, you can also provide your own placeholder view while the image is awaiting the download. For example, I am going to show the progress view while the download is in progress.

The loading state is not limited to the ProgressView. You can even show the random colors while the download is in progress to add a bit of thrill to it.


let urlString = "https://images.unsplash.com/photo-1581274998394-10ac73d6a122?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80"

AsyncImage(url: URL(string: urlString)) { image in
    image
} placeholder: {
    ProgressView()
}
0:00
/

Resizing the Downloaded Image

Sometimes the image is too large or too small to fit in a given area. This might result in some part of the image getting chopped or some area remaining white. This API also allows resizing the downloaded image before showing it on the view.

First, we will make the downloaded image resizable by calling resizable API on it and assign a fixed-sized frame to make it fit within a fixed-sized area.


let urlString = "https://images.unsplash.com/photo-1581274998394-10ac73d6a122?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80"

AsyncImage(url: URL(string: urlString)) { image in
    image.resizable()
} placeholder: {
    ProgressView()
}
.frame(width: 400, height: 400, alignment: .center)

More Control Over Loading State

To gain more control over the loading state, this API provides the content closure which returns the current downloading phase. We will use init(url:scale:transaction:content:) API for this purpose.

There are 3 possible states,

  1. Error occurred
  2. Image successfully downloaded
  3. Loading in progress

Based on the current phase value, you can show the appropriate view in the app.


AsyncImage(url: url,
           transaction: .init(animation: .spring())) { phase in

        if let image = phase.image {
            image
                .resizable()

        } else if phase.error != nil {

            Color.red.transition(.opacity.combined(with: .scale))

        } else {

            ProgressView().transition(.opacity.combined(with: .scale))

        }
}

0:00
/

As seen in the above video, we start with the loading state. If an error occurred while downloading the image, we replace it with a red color. Once the image downloads with success, we replace it with the downloaded image.

Customizing Image

The new API also provides multiple ways to customize the downloaded image before showing it on the viewport. For example, as we saw above, we can resize it, change its aspect ratio, or even apply the custom mask to it.

💡
One thing to note is, that we need to apply the transformation to AsyncImage object and NOT the downloaded image object

AsyncImage(url: URL(string: urlString), content: { image in
    image
        .resizable()
        .aspectRatio(contentMode: .fill)
}, placeholder: {
    ProgressView()
})
.frame(width: 300, height: 300, alignment: .center)
.mask(RoundedRectangle(cornerRadius: 44))

Adding Animations

Loading images using AsyncImage API shouldn't be a boring task. Let's make it more interesting by adding some animation to it. Animations can be added directly to the final or intermediate state and they are applied while those views are being displayed.

For example, I want to animate the loading view and loaded images using animation that combines their opacity and changing scales.

To enable animations, we are going to use init(url:scale:transaction:content:) API passing desired animation type in the transaction parameter.


AsyncImage(url: URL(string: urlString), transaction: .init(animation: .spring())) { phase in
	// Proceed with a phase
}

To animate the image or the placeholder view, use the transition API passing a single or combination of animations.


image
    .transition(.opacity.combined(with: .scale))

Let's combine it all in the code,


let urlString = "https://images.unsplash.com/photo-1563779480177-20d7c3ab233b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2342&q=80"

AsyncImage(url: URL(string: urlString),
           transaction: .init(animation: .spring())) { phase in
        if let image = phase.image {
            image
                .resizable()
                .transition(.opacity.combined(with: .scale))
        } else if phase.error != nil {
            Color.red.transition(.opacity.combined(with: .scale))
        } else {
            ProgressView().transition(.opacity.combined(with: .scale))
        }
}
           .frame(width: 300, height: 300, alignment: .center)
           .mask(RoundedRectangle(cornerRadius: 44))
           
           
0:00
/

Making a Picture Grid Using Grid and AsyncImage API

Now that we understood how to use the AsyncImage API in iOS 15, let's build a simple grid view using this API which loads a bunch of random images in the app.

In addition to loading, we are also going to apply various effects we discussed above,


struct Photo: Identifiable {
    let id: String
    let url: URL?
}

struct ContentView: View {
    
    var photos: [Photo] = []
    
    init() {
        for i in 0...30 {
            photos.append(Photo(id: String(i), url: URL(string: "https://picsum.photos/200/300")))
        }
    }
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.adaptive(minimum: 140))]) {
                ForEach(photos) { photo in
                    AsyncImage(url: photo.url,
                               transaction: .init(animation: .spring())) { phase in
                            if let image = phase.image {
                                image
                                    .resizable()
                                    .transition(.opacity.combined(with: .scale))
                            } else if phase.error != nil {
                                Color.red.transition(.opacity.combined(with: .scale))
                            } else {
                                ProgressView().transition(.opacity.combined(with: .scale))
                            }
                    }
                               .frame(width: 180, height: 180, alignment: .center)
                               .mask(RoundedRectangle(cornerRadius: 34))
                }
            }
        }
    }
}

0:00
/

Summary

iOS 15 has a great addition in the form of AsyncImage API which allows downloading remote images in an async manner without any 3rd party dependency. We also saw that it's not limited to it, but we can also extend this feature by applying custom transformations and animation to make it a delightful and smooth experience for the end-users.

If you've already migrated to iOS 15, I strongly recommend replacing 3rd party libraries to use AsyncImage API instead. Have you already started using it? How was your experience with it? Did you run into any gotchas and do you know any more tricks or hidden APIs to share?

Please let me know on Twitter @jayeshkawli. Looking forward to your feedback and comments about this article.

References:

Async Image API in iOS 15 - Apple Documentation

What’s new in SwiftUI - WWDC21 - Videos - Apple Developer
There’s never been a better time to develop your apps with SwiftUI. Discover the latest updates to the UI framework — including lists,...