imageswiftuiaspect-ratioclippinggeometryreader

How to scale an image to fill the parent view without affecting the layout in SwiftUI?


Goal

I often have the case where I want to scale an image so that it fills its container proportionally without modifying its frame. An example is seen in the first screenshot below. The container view has a fixed frame as indicated by the red border. The image itself has a different aspect ratio. It's scaled to fill the entire container, but it still has the same frame as the container and thus does not affect the container's layout (size).

My Solution

I used the following code to accomplish this:

struct ContentView: View {
    var body: some View {
        ImageContainerView()
            .frame(width: 300, height: 140)
            .border(.red, width: 2)
    }
}

struct ImageContainerView: View {
    var body: some View {
        GeometryReader { geometry in
            Image("image")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .border(.blue, width: 2)
                .frame(width: geometry.size.width, height: geometry.size.height)
                .clipped()
        }
    }
}

Problem

As you can see, I used a geometry reader to manually set the image's frame to match the parent view's frame. This feels a bit superfluous as the image already receives this information implicitly as the proposed size from its parent view. However, if I don't use the geometry reader this way, the image is not clipped and its frame matches the full scaled image, not the parent view's frame. This is shown in the second screenshot below.

Question

Is there a (more "native") way in SwiftUI to achieve the desired behavior without using a geometry reader?

Clipped Unclipped


Solution

  • Here is a way to do it without using GeometryReader. By making the image an .overlay() of another view, that view can handle the clipping with the .clipped() modifier:

    struct ContentView: View {
        var body: some View {
            ImageContainerView()
                .frame(width: 300, height: 140)
                .border(.red, width: 2)
        }
    }
    
    struct ImageContainerView: View {
        var body: some View {
            Color.clear
                .overlay (
                    Image("image")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .border(.blue, width: 2)
                )
                .clipped()
        }
    }
    

    Also, pass in the name of the image so that ImageContainerView and be reused with other images.