aspectfit

SwiftUI aspectRatio, scaledToFit and scaledToFill

In SwiftUI, aspect fit is .scaledToFit() and aspect fill is .scaledToFill(). Both are shorthands for .aspectRatio(contentMode:), and both do nothing useful to an Image until you add .resizable().

Start with .resizable()

An Image is drawn at its own size in points by default, whatever frame it is given. .resizable() turns it into a view that accepts any size, and only then do the aspect modifiers have something to work with:

SwiftUI
// 1. Natural size: a 1200-point-wide image stays 1200 points wide
Image("harbor")

// 2. Stretched to the frame, ratio ignored (like UIKit .scaleToFill)
Image("harbor")
    .resizable()
    .frame(width: 400, height: 300)

// 3. Aspect fit inside the frame
Image("harbor")
    .resizable()
    .scaledToFit()
    .frame(width: 400, height: 300)

Order matters because each modifier wraps the view before it. .resizable() must come directly after Image: it is a method on Image itself, not a general view modifier, so it will not compile after .frame.

.aspectRatio(contentMode:) and its shorthands

The full modifier is aspectRatio(_ aspectRatio: CGFloat? = nil, contentMode: ContentMode), with a CGSize overload. ContentMode has two cases, .fit and .fill. When the ratio is nil, SwiftUI uses the view's own ideal ratio, which for an image is the image's ratio.

You writeSame asResult in a 400 × 300 frame (1200 × 800 image)
.scaledToFit().aspectRatio(contentMode: .fit)400 × 266.67, centered, empty bands
.scaledToFill().aspectRatio(contentMode: .fill)450 × 300, overflowing the frame by 25 on each side
.aspectRatio(16/9, contentMode: .fit)the largest 16:9 size inside the frame: 400 × 225 (image stretched to 16:9)

The last row is easy to misread: passing a ratio that differs from the image's own ratio distorts the image, because the image is resizable and is told to be 16:9. Use an explicit ratio for views that have no ratio of their own (colors, shapes, video players, containers), and leave it out for images.

fit / contain
400300400266.67
fill / cover
400300450300
scaledToFit (fit) and scaledToFill (fill) in a 400 × 300 frame.

Fill a frame and crop the rest

.scaledToFill() makes the image larger than the frame on one axis, and SwiftUI draws outside a frame unless told otherwise. Add .clipped() after the frame:

SwiftUI
Image("harbor")
    .resizable()
    .scaledToFill()
    .frame(width: 400, height: 300)
    .clipped()

For rounded corners use .clipShape(RoundedRectangle(cornerRadius: 12)) instead of .clipped(); it clips to the shape.

To keep a different part of the image, give the frame an alignment. The frame positions its oversized child according to it, so .top keeps the top edge and crops only from the bottom:

SwiftUI
Image("portrait")
    .resizable()
    .scaledToFill()
    .frame(width: 400, height: 220, alignment: .top)
    .clipped()

Taps outside the visible image

.clipped() changes what is drawn, not where the view receives touches. A filled image inside a button or with .onTapGesture can still catch taps in the cropped-off area that overlaps its neighbors. Restrict hit testing to the frame with a content shape:

SwiftUI
Image("harbor")
    .resizable()
    .scaledToFill()
    .frame(width: 160, height: 160)
    .clipped()
    .contentShape(Rectangle())
    .onTapGesture { open(photo) }

Square thumbnails in a grid

A filled image reports its full, overflowing size to its parent, which can push grid cells apart. A reliable pattern is to let a clear, square view define the layout and put the image in an overlay, which does not influence the size:

SwiftUI
let columns = [GridItem(.adaptive(minimum: 110), spacing: 2)]

LazyVGrid(columns: columns, spacing: 2) {
    ForEach(photos) { photo in
        Color.clear
            .aspectRatio(1, contentMode: .fit)   // the cell is a square
            .overlay {
                Image(photo.name)
                    .resizable()
                    .scaledToFill()
            }
            .clipped()
    }
}

The trailing-closure form of overlay needs iOS 15 or macOS 12. Change the ratio (for example 4/5) for portrait cards.

Giving any view a ratio

aspectRatio is not only for images. With .fit the view becomes the largest rectangle of that ratio inside the space offered; with .fill, the smallest rectangle of that ratio that covers it.

SwiftUI
RoundedRectangle(cornerRadius: 8)
    .fill(.gray.opacity(0.2))
    .aspectRatio(16/9, contentMode: .fit)    // a 16:9 placeholder, full width
    .overlay { ProgressView() }

AsyncImage

AsyncImage (iOS 15 and later) hands you an Image in its content closure, so the same modifiers apply. Put the frame and clipping on the AsyncImage so the placeholder gets the same size:

SwiftUI
AsyncImage(url: photo.url) { image in
    image
        .resizable()
        .scaledToFill()
} placeholder: {
    Color.gray.opacity(0.15)
}
.frame(width: 120, height: 120)
.clipShape(RoundedRectangle(cornerRadius: 10))

Scale down only, fit width, fit height

SwiftUI has no single modifier for "shrink large images but never enlarge small ones", or for "scale by width and let the height overflow". Compute the scale with the same formulas and set the size explicitly; it is three lines and leaves no doubt about the result:

SwiftUI
let imageSize = CGSize(width: 1200, height: 800)     // in points
let box = CGSize(width: 400, height: 300)
let s = min(1, box.width / imageSize.width, box.height / imageSize.height)

Image("harbor")
    .resizable()
    .frame(width: imageSize.width * s, height: imageSize.height * s)
    .frame(width: box.width, height: box.height)

The visualizer writes this code for the box and image sizes you enter, including the alignment.

SwiftUI and UIKit side by side

BehaviorSwiftUIUIKit
Aspect fit.resizable().scaledToFit().scaleAspectFit
Aspect fill.resizable().scaledToFill() + .clipped().scaleAspectFill + clipsToBounds = true
Stretch.resizable().scaleToFill (default)
Natural sizeImage without .resizable().center, .top, .topLeft and the other position modes
Anchor the crop.frame(…, alignment:)no built-in option

The math behind both columns is explained in aspect fit vs aspect fill.

Questions

Why does .scaledToFit() not resize my Image?

An Image keeps its natural size until you call .resizable(). Write Image("name").resizable().scaledToFit(); resizable must come directly after Image.

Is .scaledToFill() the same as .aspectRatio(contentMode: .fill)?

Yes. scaledToFit() and scaledToFill() are shorthands for aspectRatio(contentMode: .fit) and aspectRatio(contentMode: .fill) with no explicit ratio, so the image's own ratio is used.

How do I stop a scaledToFill image from overflowing its frame?

Add .clipped() (or .clipShape for rounded corners) after the .frame modifier. Add .contentShape(Rectangle()) as well if the view handles taps, because clipping does not shrink the tap area.