The Problem
SwiftUI’s AsyncImage is convenient for loading remote images, but it has a surprising limitation: you cannot apply the .resizable() modifier to it. This code compiles but does not behave as expected:
// This does NOT work as intended
AsyncImage(url: imageURL)
.resizable() // Has no effect -- AsyncImage is not an Image
.aspectRatio(contentMode: .fill)
.frame(width: 200, height: 200)The reason is that AsyncImage is not an Image – it is a container view that manages loading state. The .resizable() modifier is defined only on Image, so applying it to AsyncImage just calls the generic View version, which does nothing useful.
The Solution
The fix is to use the phase-based initializer, which gives you direct access to the underlying Image value once loading completes:
AsyncImage(url: imageURL) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
case .failure:
Image(systemName: "photo")
.foregroundStyle(.secondary)
case .empty:
ProgressView()
@unknown default:
EmptyView()
}
}
.frame(width: 200, height: 200)
.clipped()Inside the .success case, image is a real Image value, so .resizable() works correctly. This also gives you control over the loading and error states, which is better practice anyway.
When to Load Manually
If you need the raw image data – for example, to cache it, inspect its size, or create a UIImage – you may want to skip AsyncImage entirely and load with URLSession. But for most display-only cases, the phase-based initializer covers the need without extra complexity.
This is one of those SwiftUI APIs where the simple initializer looks appealing but falls short in practice. Default to the phase-based version whenever you need any image-specific modifiers.
