AsyncImage improvements in iOS 27

Since its introduction in iOS 15, SwiftUI's AsyncImage has provided a convenient way to load and display an image from a URL. It works well when the URL is all we need, but it has offered little control over the request used to fetch the image or the URL session that performs the download.

iOS 27 adds new AsyncImage initializers that accept a URLRequest instead of a URL. We can now configure request headers, cache policy, and timeout interval while continuing to use the built-in loading and phase handling provided by AsyncImage. A new asyncImageURLSession(_:) modifier also lets us provide the URL session used by asynchronous images in a view hierarchy.

# Loading an image with a URLRequest

The simplest new initializer, init(request:scale:), takes a non-optional URLRequest, giving us control over how a particular image is fetched without having to build a separate image loader. This is useful for images served by authenticated endpoints, as well as requests that need their own cache policy or timeout.

In the example below, the request includes the access token required by the server, uses cached image data when it's available, and allows the load to run for up to 15 seconds.

struct ThumbnailView: View {
    let imageURL: URL
    let accessToken: String

    var request: URLRequest {
        var request = URLRequest(
            url: imageURL,
            cachePolicy: .returnCacheDataElseLoad,
            timeoutInterval: 15
        )
        request.setValue(
            "Bearer \(accessToken)",
            forHTTPHeaderField: "Authorization"
        )
        return request
    }

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

AsyncImage displays its default placeholder while the request is in progress and replaces it with the downloaded image when loading succeeds. If the request fails, the default placeholder remains visible, just as it does with the original URL-based initializer.

The existing content and placeholder form of AsyncImage has a new request-based equivalent, init(request:scale:content:placeholder:). It lets us modify the loaded image and replace the default placeholder while the request is in progress.

AsyncImage(request: request) { image in
    image
        .resizable()
        .scaledToFill()
} placeholder: {
    ProgressView()
}
.frame(width: 120, height: 120)
.clipShape(.rect(cornerRadius: 16))

The request parameter is optional in this form. When it's nil, AsyncImage continues to display the placeholder without starting a load.

# Responding to loading phases

For interfaces that need to distinguish between loading and failure, we can use init(request:scale:transaction:content:), which passes an AsyncImagePhase value to its content closure.

AsyncImage(
    request: request,
    transaction: Transaction(animation: .easeInOut)
) { phase in
    switch phase {
    case .empty:
        ProgressView()

    case let .success(image):
        image
            .resizable()
            .scaledToFit()
            .transition(.opacity)

    case .failure:
        ContentUnavailableView(
            "Image unavailable",
            systemImage: "photo.badge.exclamationmark"
        )

    @unknown default:
        EmptyView()
    }
}

This initializer behaves like its URL-based counterpart. The phase is empty while the request is nil or the load is in progress, and changes to either success or failure when the operation completes. The transaction controls animations associated with the phase change.

# Providing a custom URLSession

Configuring an individual URLRequest is useful when loading behavior varies from image to image. When the same networking configuration should apply to a group of images, we can create a URLSession and add it to their enclosing view with asyncImageURLSession(_:).

enum ImageSessions {
    static let gallery: URLSession = {
        let configuration = URLSessionConfiguration.default
        configuration.urlCache = URLCache(
            memoryCapacity: 20 * 1024 * 1024,
            diskCapacity: 100 * 1024 * 1024
        )
        configuration.requestCachePolicy = .useProtocolCachePolicy

        return URLSession(configuration: configuration)
    }()
}

struct ImageGallery: View {
    let imageURLs: [URL]

    var body: some View {
        ScrollView {
            LazyVGrid(
                columns: [
                    GridItem(.adaptive(minimum: 120))
                ]
            ) {
                ForEach(imageURLs, id: \.self) { url in
                    AsyncImage(url: url) { image in
                        image
                            .resizable()
                            .scaledToFill()
                    } placeholder: {
                        ProgressView()
                    }
                    .frame(height: 120)
                    .clipShape(.rect(cornerRadius: 16))
                }
            }
        }
        .asyncImageURLSession(ImageSessions.gallery)
    }
}

All AsyncImage views contained in the modified view use the supplied session for their image download data tasks. The modifier works with both the original URL-based initializers and the new request-based ones, so we can combine shared session configuration with request-specific headers or policies where needed.

These additions keep AsyncImage simple while removing two of its most significant limitations. We can now use AsyncImage for many authenticated, cache-sensitive, or specially configured image requests that would previously have required a custom loader.


If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects. And my new book The SwiftUI Way helps you adopt recommended patterns, avoid common pitfalls, and use SwiftUI's native tools appropriately to work with the framework rather than against it.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

The SwiftUI Way by Natalia Panferova book coverThe SwiftUI Way by Natalia Panferova book cover

Work with SwiftUI. Not against it.$35

A field guide to SwiftUI patterns and anti-patterns

The SwiftUI Wayby Natalia Panferova

  • Avoid common SwiftUI pitfalls
  • Build deeper intuition for the framework
  • Gain insights from a former SwiftUI Engineer at Apple

Work with SwiftUI. Not against it.

A field guide to SwiftUI patterns and anti-patterns

The SwiftUI Way by Natalia Panferova book coverThe SwiftUI Way by Natalia Panferova book cover

The SwiftUI Way

by Natalia Panferova

$35