BS
BleepingSwift
Published on

> How to Resize an Image in SwiftUI

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

Images in SwiftUI display at their original size by default. To resize them, you need to make the image resizable first, then apply sizing modifiers. The order of modifiers matters—getting it wrong results in images that won't resize at all or that stretch in unexpected ways.

Making an Image Resizable

By default, SwiftUI images ignore frame modifiers:

// This won't resize the image
Image("photo")
    .frame(width: 100, height: 100)

The image still displays at its intrinsic size. To enable resizing, add the .resizable() modifier:

Image("photo")
    .resizable()
    .frame(width: 100, height: 100)

Now the image fills the 100x100 frame, though it may appear stretched if the aspect ratio doesn't match.

Preserving Aspect Ratio

To resize while maintaining proportions, combine .resizable() with .scaledToFit() or .scaledToFill():

// Fits entirely within the frame, may leave empty space
Image("photo")
    .resizable()
    .scaledToFit()
    .frame(width: 200, height: 200)

// Fills the frame completely, may crop edges
Image("photo")
    .resizable()
    .scaledToFill()
    .frame(width: 200, height: 200)

With scaledToFit, the entire image is visible but may not fill the frame. With scaledToFill, the frame is completely filled but parts of the image may extend beyond the frame bounds.

Clipping Overflow

When using scaledToFill, the image often extends beyond its frame. Add .clipped() to cut off the overflow:

Image("photo")
    .resizable()
    .scaledToFill()
    .frame(width: 200, height: 200)
    .clipped()

For rounded corners, use .clipShape() instead:

Image("photo")
    .resizable()
    .scaledToFill()
    .frame(width: 200, height: 200)
    .clipShape(RoundedRectangle(cornerRadius: 12))

aspectRatio Modifier

For more control, use the .aspectRatio() modifier with a content mode:

// Same as scaledToFit
Image("photo")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .frame(width: 200, height: 200)

// Same as scaledToFill
Image("photo")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: 200, height: 200)

You can also specify a custom aspect ratio:

// Force a 16:9 aspect ratio
Image("photo")
    .resizable()
    .aspectRatio(16/9, contentMode: .fit)
    .frame(maxWidth: .infinity)

Flexible Sizing

Instead of fixed dimensions, let images flex based on available space:

Image("photo")
    .resizable()
    .scaledToFit()
    .frame(maxWidth: .infinity)  // Full width, height adjusts

Or set a maximum size while allowing smaller:

Image("photo")
    .resizable()
    .scaledToFit()
    .frame(maxWidth: 300, maxHeight: 200)

SF Symbols

System symbols work differently—they scale based on font size rather than frame:

// Scale with font
Image(systemName: "star.fill")
    .font(.system(size: 50))

// Or use imageScale
Image(systemName: "star.fill")
    .imageScale(.large)

If you need precise control over symbol size, you can make them resizable:

Image(systemName: "star.fill")
    .resizable()
    .frame(width: 44, height: 44)

AsyncImage Sizing

AsyncImage handles sizing slightly differently because the image might not exist yet:

AsyncImage(url: imageURL) { image in
    image
        .resizable()
        .scaledToFill()
} placeholder: {
    Rectangle()
        .fill(Color.gray.opacity(0.3))
}
.frame(width: 200, height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))

Apply the frame to the AsyncImage container, and apply resizable() to the loaded image inside the closure.

Common Patterns

A profile avatar with a circular crop:

struct AvatarView: View {
    let imageURL: URL?

    var body: some View {
        AsyncImage(url: imageURL) { image in
            image
                .resizable()
                .scaledToFill()
        } placeholder: {
            Circle()
                .fill(Color.gray.opacity(0.3))
        }
        .frame(width: 60, height: 60)
        .clipShape(Circle())
    }
}

A hero image that spans full width:

struct HeroImage: View {
    let imageName: String

    var body: some View {
        Image(imageName)
            .resizable()
            .scaledToFill()
            .frame(height: 250)
            .frame(maxWidth: .infinity)
            .clipped()
    }
}

A thumbnail grid:

struct ThumbnailGrid: View {
    let images: [String]

    var body: some View {
        LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 8) {
            ForEach(images, id: \.self) { imageName in
                Image(imageName)
                    .resizable()
                    .scaledToFill()
                    .frame(width: 100, height: 100)
                    .clipShape(RoundedRectangle(cornerRadius: 8))
            }
        }
    }
}

Interpolation Quality

For pixel art or images that shouldn't be smoothed when scaled, adjust the interpolation:

Image("pixel-art")
    .resizable()
    .interpolation(.none)  // Preserves sharp pixel edges
    .frame(width: 200, height: 200)

The default interpolation smooths edges when scaling, which is usually desirable for photos but ruins pixel art aesthetics.

Template Rendering

To tint an image with a color, use template rendering mode:

Image("icon")
    .resizable()
    .renderingMode(.template)
    .frame(width: 44, height: 44)
    .foregroundStyle(.blue)

This is useful for icons that need to match your app's color scheme.

Modifier Order Matters

The order of modifiers is critical with images. This works:

Image("photo")
    .resizable()      // 1. Make resizable
    .scaledToFill()   // 2. Set content mode
    .frame(...)       // 3. Apply size
    .clipped()        // 4. Clip overflow

This doesn't work as expected:

Image("photo")
    .frame(...)       // Frame has no effect yet
    .resizable()      // Now it's resizable but frame already applied

Always start with .resizable(), then content mode, then frame, then clipping.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.