iOS
June 8, 2021

AsyncImage. Loading images in SwiftUI

The article is now available on my blog:

https://www.artemnovichkov.com/blog/async-image

Note: Examples are tested on iOS 15.0 with Xcode 13.0 beta (13A5154h).

iOS 15.0 beta gives us new SwiftUI views, and one of them is AsyncImage. It loads and displays an image from the given URL.

Let's start with a basic example:

import SwiftUI

struct ContentView: View {

    private let url = URL(string: "https://picsum.photos/200")

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

By default, it shows a gray background and replaces it with the loaded image:

Empty and success states of AsyncImage

Optionally we can change scale to use for the image. In the example below the image size will be reduced by half:

import SwiftUI

struct ContentView: View {

    private let url = URL(string: "https://picsum.photos/200")

    var body: some View {
        AsyncImage(url: url, scale: 2)
    }
}

To update the appearance of AsyncImage, we can use an initializer with content and placeholder view builders. Here we able to modify a final image and show a custom placeholder view:

import SwiftUI

struct ContentView: View {

    private let url = URL(string: "https://picsum.photos/200")

    var body: some View {
        AsyncImage(url: url) { image in
            image
                .resizable()
                .aspectRatio(contentMode: .fit)
        } placeholder: {
            Image(systemName: "photo")
                .imageScale(.large)
                .foregroundColor(.gray)
        }
        .ignoresSafeArea()
    }
}
Custom placeholder and resized image

If we want to handle an error state, we can use another initializer with AsyncImagePhase. It's a simple enum with three cases: empty, success, and error.

import SwiftUI

struct ContentView: View {

    private let url = URL(string: "https://picsum.photos/200")

    var body: some View {
        AsyncImage(url: url, content: view)
    }

    @ViewBuilder
    private func view(for phase: AsyncImagePhase) -> some View {
        switch phase {
        case .empty:
            ProgressView()
        case .success(let image):
            image
                .resizable()
                .aspectRatio(contentMode: .fit)
        case .failure(let error):
            VStack(spacing: 16) {
                Image(systemName: "xmark.octagon.fill")
                    .foregroundColor(.red)
                Text(error.localizedDescription)
                    .multilineTextAlignment(.center)
            }
        @unknown default:
            Text("Unknown")
                .foregroundColor(.gray)
        }
    }
}

Here we show a spinner during loading, resized image if loading is successful, and an error message if something is wrong.

Working with phases of AsyncImage

To specify animations between phase changes, we can optionally add Transition:

import SwiftUI

struct ContentView: View {

    private let url = URL(string: "https://picsum.photos/200")
    private let transaction: Transaction = .init(animation: .linear)

    var body: some View {
        AsyncImage(url: url,
                   transaction: transaction,
                   content: view)
    }
    ...
}

And, of course, we can use AsyncImage inside List to show multiple images:

import SwiftUI

struct ContentView: View {

    private let url = URL(string: "https://picsum.photos/200")

    var body: some View {
        List {
            ForEach(0..<10) { _ in
                AsyncImage(url: url,
                           content: view)
                    .listRowInsets(.init(.zero))
            }
        }
        .listStyle(.plain)
    }
    ...
}
Finally, we can show List without separators

If you want to play with AsyncImage by yourself, check out AsyncImageExample project on Github.

References


Twitter · Telegram · Github