โ† All articles

AsyncImage in iOS 27: Headers, Caching, and Custom Sessions

iOS 27 rebuilds SwiftUI's AsyncImage with URLRequest support, custom URLSessions, and phase-based errors. Load authenticated images, no custom loader.

AsyncImage in iOS 27: Headers, Caching, and Custom Sessions

For four years, SwiftUI's AsyncImage could load an image from a URL and almost nothing else. No request headers, no cache policy, no shared session. The moment your images lived behind an authenticated endpoint, you reached for a third-party loader or rolled your own. iOS 27 closes that gap: AsyncImage now accepts a full URLRequest and a custom URLSession, so the common reasons to abandon it are gone.

This post covers what changed, with working examples for the three things developers needed most: authenticated requests, shared session configuration, and proper loading-versus-error handling.

On this page

What the old AsyncImage could not do

The original AsyncImage took a URL and managed the download internally. That was fine for public images, and genuinely convenient. The problem was everything it hid. You could not set an Authorization header, you could not choose a cache policy or timeout, and you could not point it at a URLSession you had configured. The networking was a black box.

In practice that meant the first time you needed to load an avatar from an API that required a bearer token, you stopped using AsyncImage entirely and pulled in Kingfisher or Nuke, or wrote a small ObservableObject loader by hand. iOS 27 makes those detours unnecessary for most apps.

Load from a URLRequest

The headline addition is a set of initializers that take a URLRequest instead of a URL. Because you build the request yourself, every knob URLRequest exposes is now available to AsyncImage: headers, cache policy, and timeout.

var request = URLRequest(url: imageURL)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.cachePolicy = .returnCacheDataElseLoad
request.timeoutInterval = 15
 
AsyncImage(request: request, scale: 2)

That single change removes the most common reason teams replaced AsyncImage. Authenticated image endpoints, signed URLs that need a specific cache behavior, and slow CDNs that need a tighter timeout are all handled inline now.

The request-based initializers come in the same shapes as the URL ones, including the content-and-placeholder form for transforming the loaded image and showing your own placeholder while it loads.

AsyncImage(request: request) { image in
    image
        .resizable()
        .scaledToFill()
} placeholder: {
    Color.gray.opacity(0.2)
}

Practical rule: if you were using AsyncImage for anything behind auth, you no longer need a third-party image library to do it. Build a URLRequest and pass it in.

One useful detail: the request parameter is optional in the content-and-placeholder form. Passing nil shows the placeholder without kicking off a load, which is handy when the URL is not ready yet (for example, a model that has not finished fetching).

Share a URLSession across images

Per-request control is half the story. The other half is configuring the session that performs the downloads, especially the cache. iOS 27 adds the asyncImageURLSession(_:) modifier, which supplies a URLSession to every AsyncImage in a view hierarchy through the environment.

let config = URLSessionConfiguration.default
config.urlCache = URLCache(
    memoryCapacity: 20_000_000,
    diskCapacity: 200_000_000
)
let session = URLSession(configuration: config)
 
ScrollView {
    LazyVStack {
        ForEach(photos) { photo in
            AsyncImage(request: URLRequest(url: photo.url))
        }
    }
}
.asyncImageURLSession(session)

Now every image in that list shares one session and one cache, instead of each download going through the default shared session with its modest default cache. For an image-heavy feed, that is the difference between re-downloading on every scroll and serving from disk.

You can combine the two features freely: a shared session for cache configuration, plus a per-image URLRequest for headers. The session sets the policy for the group; the request customizes each load.

Handle loading and error phases

The request initializer that takes a transaction and a content closure passes you an AsyncImagePhase, so you can render loading, success, and failure distinctly, and animate the transition between them.

AsyncImage(
    request: URLRequest(url: imageURL),
    transaction: Transaction(animation: .easeInOut)
) { phase in
    switch phase {
    case .empty:
        ProgressView()
    case .success(let image):
        image
            .resizable()
            .scaledToFit()
    case .failure:
        ContentUnavailableView(
            "Couldn't load image",
            systemImage: "photo.badge.exclamationmark"
        )
    @unknown default:
        EmptyView()
    }
}

The failure case is the meaningful upgrade here. The old phase-based initializer existed, but pairing it with a real URLRequest means a failed authenticated load now surfaces as a proper error state you can present with ContentUnavailableView, rather than a silent blank. The Transaction gives you control over how the success image animates in, so the swap from spinner to image is not a hard cut.

Old versus new at a glance

What you needOld AsyncImageiOS 27 AsyncImage
Load a public image URLAsyncImage(url:)Unchanged, still works
Custom headers (auth, etc.)Not possible; needed a custom loaderAsyncImage(request:)
Cache policy and timeoutNot possible inlineSet them on the URLRequest
Shared session and cacheBuild your own pipeline.asyncImageURLSession(_:)
Distinguish loading vs failurePhase closure, but no request controlAsyncImagePhase with a real request
Skip loading when URL is absentAwkwardPass a nil request

The URL-based initializers are all still there. The request-based ones are additions, not replacements, so existing code keeps compiling.

When to reach for what

For a public image with no special requirements, keep using AsyncImage(url:). It is the least code and nothing changed.

Reach for the URLRequest initializers when the endpoint needs a header, a specific cache policy, or a tighter timeout. Add asyncImageURLSession(_:) when you have a screen full of images that should share a cache, like a feed or a grid. Use the phase-plus-transaction initializer when a failed load needs a visible, designed error state rather than an empty frame.

You will still want a dedicated image library when you need advanced on-the-fly processing, format-specific decoding, or a prefetching pipeline tuned for very large lists. For the everyday case of "load images, some of them behind auth, and cache them sensibly," iOS 27 means AsyncImage is finally enough. If you are starting a fresh project, our guide to getting started with SwiftUI covers the surrounding view layer, and the Xcode 27 complete guide rounds up the rest of this year's changes.

FAQ

Does AsyncImage still accept a plain URL?

Yes. The AsyncImage(url:) initializers are unchanged and still work exactly as before. The URLRequest initializers are additive, so you only adopt them where you need the extra control.

What iOS version do the new initializers require?

iOS 27. The URLRequest initializers and the asyncImageURLSession(_:) modifier are new this cycle, so guard with an availability check if you still support iOS 26 and earlier.

Can I pass a nil request?

Yes, in the content-and-placeholder form. A nil request shows the placeholder without starting a load, which is useful when the URL is not available yet.

Does asyncImageURLSession apply to every AsyncImage on the screen?

It applies to every AsyncImage in the view hierarchy below the modifier, through the environment. Set it once on a container and all the images inside share that session and its cache.

Do I still need Kingfisher or Nuke?

For most apps, no longer. Authenticated requests, cache configuration, and error states were the usual reasons to switch, and iOS 27 covers all three. A dedicated library still earns its place for heavy image processing or a custom prefetching pipeline.

For four years the honest answer to "should I use AsyncImage in production" was "only for simple cases." iOS 27 changes that answer. With URLRequest support, a configurable shared session, and proper phase handling, the framework now covers the requirements that used to force a third-party dependency, with less code and no extra package to maintain.

This post is based on the WWDC26 SwiftUI updates and the detailed breakdown by Nil Coalescing, plus Apple's AsyncImage documentation.


Spaceport generates production-ready SwiftUI Xcode projects with the boring-but-essential parts of a paid iOS app already wired up: RevenueCat subscriptions, onboarding, sign-in, analytics, an AI assistant scaffold, and App Store Connect pricing across 25 markets. The generated code follows current SwiftUI conventions like these, so you start on iOS 27 patterns instead of retrofitting them. From an indie iOS dev, for indie iOS devs.

Read more at spaceport.build

Community appsJoin Discord