5

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

Mischa
  • 15,816
  • 8
  • 59
  • 117

2 Answers2

12

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.

Mischa
  • 15,816
  • 8
  • 59
  • 117
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • 1
    This is what I was looking for, thanks! It's only a minimal example I produced for Stackoverflow, so it doesn't have any properties or reusability implemented on purpose. – Mischa Sep 14 '22 at 08:56
  • 1
    Thanks for taking the time to write a proper question with a minimal reproducible example! – vacawama Sep 14 '22 at 09:00
  • This was wrecking my head, thanks mate. This worked perfectly – SparkyRobinson Mar 01 '23 at 09:06
1

Just move .clipped() from the child view to the parent. That is where you want the image clipped:

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

struct ImageContainerView: View {
    var body: some View {
            Image("island")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .border(.blue, width: 2)
    }
}
Yrb
  • 8,103
  • 2
  • 14
  • 44
  • I should have formulated my question better, sorry. (I wanted to produce a minimal example for Stackoverflow to make clear that a fixed frame is proposed from *outside*, but I'm looking for a solution from *inside* the view.) Oftentimes, you do not have control over the parent view. It might be the app's main view or a widget's main view, for example. In that case, you cannot clip the parent view, so this solution only works when you own the parent view. – Mischa Sep 14 '22 at 08:54