How to use AsyncImage

Let’s look at Apple documentation:

1. The most basic example.

AsyncImage(url: URL(string: "https://example.com/icon.png"))
    .frame(width: 200, height: 200)

In this case, .frame changes only layout size, but does not affect the visible image’s size.

Code for testing:

struct AsyncImageView: View {
    
    var smallImage: URL { imageUrl(pixels: 64) }
    var bigImage: URL { imageUrl(pixels: 128) }
    
    var body: some View {
        VStack {
            AsyncImage(url: smallImage)
                .frame(width: 50, height: 50)
            AsyncImage(url: bigImage)
                .frame(width: 50, height: 50)
        }
    }
    
    func imageUrl(pixels: Int) -> URL {
        URL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_logo_black.svg/\(pixels)px-Apple_logo_black.svg.png")!
    }
}

Without .frame - the created view’s layout size depends on the image resolution. So, this use case for the URL initializer is only applicable when the image resolution is known in advance and guaranteed it will not change in the future. The view is coupled with images urls and resolution, which sounds not good to me. Also instead of using apple documentation you need to explore how to use it by yourself.Definetly not for cases when you need a fast implementation.

2. Image and placeholder

AsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
    image.resizable()
} placeholder: {
    ProgressView()
}
.frame(width: 50, height: 50)

This gives you more control over the layout, independent of fetched image resolution unlike previous example. Image result can apply frame, aspectRatio, etc.However, there is one pitfall. If you use ProgressView as Apple’s docs suggests, and your url is incorrect, you will get an infinite ProgressView spinning.So, the placeholder closure is not intended for a loading indicator. It’s for a placeholder image that will appear initially, then change to the fetched image if it loads successfully. If the image fails to load, the placeholder will remain unchanged.

3. Image, loading indicator and error

using if-else:

AsyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
    if let image = phase.image {
        image // Displays the loaded image.
    } else if phase.error != nil {
        Color.red // Indicates an error.
    } else {
        Color.blue // Acts as a placeholder.
    }
}

or using an enum:

AsyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
    switch phase {
    case .success(let image):
        image.resizable()
            .scaledToFit()
    case .empty:
        ProgressView()
    case .failure:
        Image(systemName: "exclamationmark.triangle")
    @unknown default:
        Image(systemName: "photo")
    }
}

Pay attention to the non-obvious naming - .empty case is for loading state.