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?

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?

  • Image displays 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 .fit vs .fill behavior
  • .clipShape and .overlay shape images and add decorations
  • AsyncImage handles 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.