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
- Load from a URLRequest
- Share a URLSession across images
- Handle loading and error phases
- Old versus new at a glance
- When to reach for what
- FAQ
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
AsyncImagefor anything behind auth, you no longer need a third-party image library to do it. Build aURLRequestand 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 need | Old AsyncImage | iOS 27 AsyncImage |
|---|---|---|
| Load a public image URL | AsyncImage(url:) | Unchanged, still works |
| Custom headers (auth, etc.) | Not possible; needed a custom loader | AsyncImage(request:) |
| Cache policy and timeout | Not possible inline | Set them on the URLRequest |
| Shared session and cache | Build your own pipeline | .asyncImageURLSession(_:) |
| Distinguish loading vs failure | Phase closure, but no request control | AsyncImagePhase with a real request |
| Skip loading when URL is absent | Awkward | Pass 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.