iosswiftuiuiimageviewaspect-ratiogeometryreader

Image in SwiftUI takes extra space at bottom while trying to set Aspect Ratio


I want to have Image occupying full width of its container and height will be based on aspect ratio provided explicitly. Also the image should cover all its frame and doesn't matter its content is cut i.e. Aspect Fill mode as per following image.

Desired output: Aspect Fill

enter image description here

Way 1

Here is my View:

struct AspectImage: View {
    var heightMultiplier: CGFloat
    var body: some View {
        GeometryReader { proxy in
            Image(.leapord)
                .resizable()
                .scaledToFill()
                .frame(width: proxy.size.width, height: proxy.size.width * heightMultiplier)
                .clipped()
                .border(color: .black, width: 5, radius: 22)
        }
    }
}

How I am using:

struct ImageAspectContainerView: View {
    var body: some View {
          VStack {
            Text("Hi")
            AspectImage(heightMultiplier: 0.3)
                .background(.gray)
            Text("Hello")
        }
    }
}

How it looks:

enter image description here

The image aspect ratio is working as expected but the image is taking space at bottom (gray area in the screenshot).

Way 2

I have tried .aspectRatio modifier but it does not give desired output:

struct AspectImage: View {
    var aspectRatio: CGFloat
    var body: some View {
        GeometryReader { proxy in
            Image(.leapord)
                .resizable()
                .scaledToFill()
                .frame(width: proxy.size.width)
                .aspectRatio(aspectRatio, contentMode: .fill)
                .border(color: .black, width: 5, radius: 22)
        }
    }
}

struct ImageAspectContainerView: View {
    var body: some View {
        VStack {
            Text("Hi")
            AspectImage(aspectRatio: 2/1) // Aspect ratio 2:1
                .background(.gray)
            Text("Hello")
        }
    }
}

How it looks:

enter image description here

As you can see from the screenshot, the aspect ratio is not working. I first set the width to full available space and then setting aspect ratio using aspectRatio but it does not work.

Solution 1

I have tried solution provided by Benzy Neez, it's almost what I want to do but the only issue is image is streching:

struct AspectImage: View {
    
    let aspectRatio: CGFloat = 16/5
    
    var body: some View {
        ViewThatFits(in: .horizontal) {
            let theImage = Image(.leapord)
            theImage
                .resizable()
                .aspectRatio(aspectRatio, contentMode: .fill) 
            theImage
                .resizable()
                .aspectRatio(aspectRatio, contentMode: .fit)
        }
        .clipShape(RoundedRectangle(cornerRadius: 22))
        .overlay {
            RoundedRectangle(cornerRadius: 22)
                .stroke(.black, lineWidth: 5)
        }
    }
}

struct AspectImageContainer: View {
    var body: some View {
        VStack {
            Text("Hi")
            AspectImage()
                .padding(.horizontal)
            Text("Hello")
        }
    }
}

How it looks:

enter image description here


Solution

  • In your example, you are using .scaledToFill() for both your approaches. If you are trying to stretch the image by using a different aspect ratio to its natural aspect ratio, try using .aspectRatio(aspectRatio, contentMode: .fill) instead of .scaledToFill().

    However, it is the GeometryReader that is consuming all of the space available and causing the gray background to fill the screen.

    A GeometryReader can be avoided by using ViewThatFits to choose between scaled-to-fill and scaled-to-fit, depending on the aspect ratio:

    ViewThatFits(in: .horizontal) {
        let theImage = Image(.leapord)
        theImage
            .resizable()
            .scaledToFill()
        theImage
            .resizable()
            .scaledToFit()
    }
    .clipShape(RoundedRectangle(cornerRadius: 22))
    .overlay {
        RoundedRectangle(cornerRadius: 22)
            .stroke(.black, lineWidth: 5)
    }
    

    Again, if you want to apply a custom aspect ratio, then replace .scaledToFill() with .aspectRatio(aspectRatio, contentMode: .fill) and replace .scaledToFit() with .aspectRatio(aspectRatio, contentMode: .fit).

    The approach above will fill the full width, with the height being determined by the aspect ratio of the image (or by the parameter to the .aspectRatio modifier, as applicable).

    If you want to impose a maximum height, above which the image is clipped, then this needs to be applied before clipping and before the border is applied. Here is how AspectImage could be adapted to work like this:

    struct AspectImage: View {
        let image: Image
        var maxHeight: CGFloat?
    
        var body: some View {
            ViewThatFits(in: .horizontal) {
                image
                    .resizable()
                    .scaledToFill()
                image
                    .resizable()
                    .scaledToFit()
            }
            .frame(maxHeight: maxHeight)
            .fixedSize(horizontal: false, vertical: true)
            .clipShape(RoundedRectangle(cornerRadius: 22))
            .overlay {
                RoundedRectangle(cornerRadius: 22)
                    .stroke(.black, lineWidth: 5)
            }
        }
    }
    

    Example use:

    var body: some View {
        VStack {
            Text("Hi")
            AspectImage(image: Image(.image1), maxHeight: 350)
                .padding(.horizontal)
            Text("Hello")
        }
    }
    

    Screenshot

    See also my answer to Image is being rendered with 1/3 of screen width for a way of scaling an image to fill the available width and fit within the available height. It might be that you are after the solution Maximum width, minimum height, which is the third part of the answer.