imageswiftuigeometryreader

Show Image in full width but crop top/bottom to fill available space


I'm trying to display an image in the middle of the screen:

struct ContentView: View {
    let items = ["A", "B", "C", "D", "E", "F", "G"]
    var body: some View{
        VStack {
            ForEach(items, id: \.self) { item in
                Text(item)
            }
            Spacer() // 3.
            GeometryReader { geometry in
                Image("image-300x1200")
                    .resizable() // 1.
            }
            Button("Action") {}
        }
    }
}

I want the image to:

  1. Fill the full width of the screen, and
  2. Vertically if height is more than available space, crop top and bottom to fill available space without push the other views above and below the image beyond screen, and
  3. If the image height is less than available height, stick it to the bottom view which is the button

So far I think I've achieved 1 and 2, just not sure what modifies I need for 2:

resized image

So the image is resized without keeping aspect ratio. If I add scaledToFill() after resizable() it goes beyond the bottom of screen:

resize scaledToFill

Then I thought about using the space provided by geometry reader, like adding the following:

.aspectRatio(geometry.size.width / geometry.size.height, contentMode: .fill)

But it still doesn't work as it doesn't crop.


Solution

  • You can do:

    Image(...)
        .resizable()
        .fixedSize(horizontal: false, vertical: true)
        .aspectRatio(contentMode: .fill)
        .frame(maxHeight: geometry.size.height, alignment: .bottom)
        .clipped()
    

    .aspectRatio(contentMode: .fill) makes the image expand to fill the entire available width. Note that we do not want it to also expand vertically to fill the entire available height, hence the fixedSize modifier.

    Then, maxHeight: geometry.size.height gives it a frame with maximum available height. Note that when the height is too large, the image will exceed this frame, so it also needs to be clipped().

    alignment: .bottom so that the image stays at the bottom of the frame when the height is less than the available height.