Images in SwiftUI: SF Symbols, AsyncImage, and Custom Assets
A Pixar movie without visuals would just be a script. The same goes for apps — images bring your interface to life. Whether you’re displaying a character poster from your asset catalog, an icon from Apple’s SF Symbols library, or a movie thumbnail loaded from the internet, SwiftUI makes it straightforward.
You’ll learn how to display images with Image, use SF Symbols, load remote images with AsyncImage, and apply
modifiers like resizable(), aspectRatio, and clipShape. We won’t cover custom drawing or Core Graphics — those are
separate topics.
What You’ll Learn
- What Is the Image View?
- Asset Catalog Images
- SF Symbols
- Resizing and Scaling
- Clipping and Overlays
- AsyncImage for Remote Images
- Common Mistakes
- What’s Next?
What Is the Image View?
The Image view displays a picture in your SwiftUI layout. It can load images from three sources: your app’s asset
catalog, the SF Symbols icon library, or the system.
Apple Docs:
Image— SwiftUI
Think of Image like a picture frame on Andy’s bedroom wall. The frame (Image view) holds the picture, and you choose
which picture to put in it — a photo from your album (asset catalog), a sticker (SF Symbol), or a download from the
internet (AsyncImage).
Asset Catalog Images
The most common way to display images is from your asset catalog — the Assets.xcassets folder in Xcode. You drag
images there and reference them by name.
struct MoviePosterView: View {
var body: some View {
Image("woody-poster")
}
}
This loads the image named “woody-poster” from your asset catalog. If the image doesn’t exist in the catalog, SwiftUI shows nothing — no crash, just a blank space. If you want your first SwiftUI view to show custom images, start by adding them to the asset catalog.
Tip: Use Xcode’s asset catalog to provide 1x, 2x, and 3x versions of your images. The system automatically picks the right one for the device’s screen density.
SF Symbols
SF Symbols is Apple’s library of thousands of free, scalable icons that match the iOS design language. You create
them with Image(systemName:).
struct IconGalleryView: View {
var body: some View {
HStack(spacing: 20) {
Image(systemName: "star.fill")
Image(systemName: "heart.fill")
Image(systemName: "film")
}
.font(.largeTitle)
}
}
This displays three icons in a row: a filled star, a filled heart, and a film frame. SF Symbols behave like text — they
scale with .font() and inherit the foreground color.
Customizing SF Symbols
You can change the color, size, and rendering mode of SF Symbols.
struct RatingView: View {
var body: some View {
HStack {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
Image(systemName: "star")
.foregroundStyle(.gray)
}
.font(.title)
}
}
This creates a rating display with two filled yellow stars and one empty gray star — perfect for rating your favorite Pixar movie.
Tip: Download the free SF Symbols app from Apple to browse all available icons and find the perfect one for your feature.
Resizing and Scaling
By default, Image renders at the image’s natural size. For asset catalog images, this can be enormous. You need two
modifiers to control the size: .resizable() and a sizing modifier.
struct ResizedPosterView: View {
var body: some View {
Image("buzz-poster")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 300)
}
}
.resizable()tells SwiftUI the image can be stretched or shrunk. Without it, the image always renders at its native pixel size..aspectRatio(contentMode: .fit)scales the image to fit within the frame while preserving its proportions..frame(width:height:)sets the bounding box.
Fit vs. Fill
The contentMode parameter controls how the image fits into the available space:
// Fit — entire image visible, may have empty space
Image("coco-poster")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
// Fill — fills the entire frame, may crop edges
Image("coco-poster")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 200, height: 200)
.clipped()
With .fit, the whole image is visible but may leave blank space. With .fill, the image fills every pixel of the
frame but edges may be cut off. Add .clipped() when using .fill to prevent the image from overflowing its frame.
Clipping and Overlays
You can shape images with .clipShape and layer content on top with .overlay.
struct CharacterAvatarView: View {
var body: some View {
Image("remy-avatar")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.clipShape(Circle())
.overlay(
Circle().stroke(.blue, lineWidth: 3)
)
}
}
This creates a circular avatar with a blue border — the kind you’d see in a profile screen. .clipShape(Circle()) crops
the image into a circle, and .overlay adds a ring on top. You can learn more layout techniques in
SwiftUI Layout System.
AsyncImage for Remote Images
AsyncImage loads an image from a URL over the network. It handles the download, caching, and loading state for you.
Apple Docs:
AsyncImage— SwiftUI
struct RemotePosterView: View {
var body: some View {
AsyncImage(url: URL(string: "https://example.com/walle-poster.jpg")) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
ProgressView()
}
.frame(width: 200, height: 300)
}
}
While the image downloads, the placeholder shows a spinning progress indicator. Once loaded, the image closure
receives the downloaded Image view that you can customize with modifiers.
Handling Errors
Use the AsyncImagePhase variant for more control over loading, success, and failure states.
struct RobustImageView: View {
var body: some View {
AsyncImage(url: URL(string: "https://example.com/nemo.jpg")) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
case .failure:
Image(systemName: "photo")
.foregroundStyle(.gray)
@unknown default:
EmptyView()
}
}
.frame(width: 150, height: 150)
}
}
The phase enum gives you three cases: .empty (still loading), .success (image loaded), and .failure (something
went wrong). This lets you show a placeholder icon when the download fails.
Common Mistakes
Forgetting resizable() Before Sizing Modifiers
Without .resizable(), modifiers like .frame() and .aspectRatio() don’t resize the image — they only change the
frame around it.
// ❌ Don't do this — image stays at natural size
Image("poster")
.frame(width: 100, height: 100)
// ✅ Do this — image scales to fit the frame
Image("poster")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
Always call .resizable() first when you want the image to change size.
Using AsyncImage for Local Images
AsyncImage is for downloading images from the internet. For images in your asset catalog, use the regular Image
view.
// ❌ Don't do this — unnecessary network overhead for local assets
AsyncImage(url: Bundle.main.url(forResource: "woody", withExtension: "png"))
// ✅ Do this — load local assets directly
Image("woody")
Local images load instantly from the app bundle. There’s no reason to go through the async loading path.
What’s Next?
Imagedisplays pictures from asset catalogs, SF Symbols, or the system- SF Symbols are scalable icons that behave like text
.resizable()is required before any sizing modifiers.aspectRatio(contentMode:)controls.fitvs.fillbehavior.clipShapeand.overlayshape images and add decorationsAsyncImagehandles downloading, caching, and loading states for remote images
Ready to connect your app to the internet and fetch real data? Head over to
Networking in Swift to learn about URLSession, JSON decoding with Codable,
and displaying remote data in your views.