AsyncImage. Loading images in SwiftUI
3 min read • ––– views
iOS 15.0 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:
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()
    }
}
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.
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)
    }
    ...
}
If you want to play with AsyncImage by yourself, check out AsyncImageExample project on Github.
