How it works
AsyncImagePhase represents the current state of an image that AsyncImage loads over the network. Because that download happens asynchronously, the view passes through a sequence of phases — no image yet, a loaded image, or a failure — and AsyncImagePhase is the value SwiftUI hands your content closure so you can render the right thing for each one. Reach for it whenever you use the closure-based AsyncImage initializer and want full control over the loading placeholder, the resolved image, and the error state, rather than accepting the default behavior.
Receive the phase in the AsyncImage content closure
The trailing-closure form of AsyncImage calls your closure each time loading progresses, passing one AsyncImagePhase value. In the example the parameter
phaseis that value, and the whole closure body re-runs as the phase advances from empty to success or failure.Switch over the three cases
AsyncImagePhase is an enum, so you handle it with a
switch. Its cases are.empty(no image has loaded yet, typically while the request is in flight),.success(the image resolved), and.failure(the load errored). Each branch returns the view to show for that state.Unwrap the loaded image from .success
The
.successcase carries an associatedImagevalue, bound here withcase .success(let image). Thatimageis a ready-to-use SwiftUIImage, so you apply normal modifiers to it — the example chains.resizable()and.scaledToFit()to size the photo within its frame.Show a placeholder for .empty and a fallback for .failure
While the phase is
.empty, render a stand-in such as theProgressView()shown here; when it is.failure, render an error fallback like theImage(systemName: "photo")styled with.font(.largeTitle)and.foregroundStyle(.secondary). This is where AsyncImagePhase earns its keep — it lets you design loading and error states yourself.Handle future cases with @unknown default
AsyncImagePhase is a non-frozen enum, so SwiftUI may add cases in later releases. Covering them with
@unknown default— returningEmptyView()in the example — keeps the switch exhaustive today while staying forward-compatible.
url at a string that doesn't resolve to an image to force the .failure branch and watch the Image(systemName: "photo") fallback appear instead of the photo.Example & preview
Press Run live & edit to compile it in your browser — then edit the Swift on the left and the preview re-renders live.
struct AsyncImagePhaseDemo: View {
var body: some View {
AsyncImage(url: URL(string: "https://example.com/photo.jpg")) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.scaledToFit()
case .failure:
Image(systemName: "photo")
.font(.largeTitle)
.foregroundStyle(.secondary)
@unknown default:
EmptyView()
}
}
.frame(width: 120, height: 120)
.padding()
}
}