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
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:
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:
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:
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")
}
}
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.