EXPEDIA GROUP TECHNOLOGY — SOFTWARE

Resizing Images in SwiftUI

How to resize images to any aspect ratio without distortion

Kari Grooms
Expedia Group Technology

--

The same image is displayed four times, each at a different size. Images are positioned above the SwiftUI code that handles resizing the images.
Image by author

In the Vrbo™️ (part of Expedia Group™) iOS app, we’ve recently converted some of our existing UIKit views over to SwiftUI and encountered issues when resizing images. The problem arises when we need to resize an image to a certain size and maintain the aspect ratio without skewing.

We initially encountered this while working on Vrbo’s iOS 14 Widget, when we needed the image to be a perfect square, and again when building a SwiftUI version of one of our property cards. We display property images on nearly every screen in the app and at varying sizes depending on the context, meaning it was important to solve this problem in a scalable way.

Decorative separator

iOS Widget

Screenshot of Vrbo’s iOS 14 Widget in light mode, implemented with SwiftUI
Vrbo’s iOS 14 Widget in light mode
Screenshot of Vrbo’s iOS 14 Widget in dark mode, implemented with SwiftUI
Vrbo’s iOS 14 Widget in dark mode

Property Card

Screenshot of Vrbo’s iOS app main screen featuring recently viewed property cards implemented with SwiftUI
Vrbo’s iOS app main screen featuring property cards
Decorative separator

The Road to Resizing

Starting from scratch, this is what you get when you render an Image in SwiftUI without any modifiers.

// original size: 338 x 360
Image("bungalows")
SwiftUI Preview of image with no view modifiers applied. The image is displayed at its original size.
SwiftUI Preview

Now things are going to start getting weird. First, we explicitly set the size via .frame().

// try to override size
Image("bungalows")
.frame(width: 100, height: 100)
SwiftUI Preview of image with a .frame() modifier applied. The image was not resized. It is still displayed at its original size; however, there is a blue rectangle indicating where the frame is, even though the image was not changed.
SwiftUI Preview

Note: the blue square is there to indicate where the frame is. This wouldn’t be visible in the UI.

Since nothing happens when you set the .frame() explicitly, let’s see what applying the .clipped() modifier gets us.

// this sorta works
Image("bungalows")
.frame(width: 100, height: 100)
.clipped()
SwiftUI Preview of image with .frame() and .clipped() modifiers applied. The image is not resized, but was cropped to the frame size.
SwiftUI Preview

Better….but our image just got cropped and was not resized. SwiftUI has a .resizable() view modifier, so what happens when we use that instead?

Image("bungalows")
.resizable()
SwiftUI Preview of the image using the .resizable() modifier. The image was resized to the size of its container; in this case, the entire height and width of the iPhone, resulting in the image being skewed and not retaining its aspect ratio.
SwiftUI Preview

Not what we want, but at least the image is resized now. By default, the resizingMode is .stretch, which is what we see above, but you could also set this to .tile.

// .stretch is default
Image("bungalows")
.resizable(resizingMode: .tile)
SwiftUI Preview of the image using the .resizable() modifier with .tile as its resizeMode. The image was still resized to the size of its container, but this time the image is tiled across the container instead of being stretched to fit, maintaining its aspect ratio.
SwiftUI Preview

The image looks better, but this is still not what we want. Let’s try explicitly setting the .frame() again, but this time in conjunction with the .resizable() modifier.

// specify an exact size
Image("bungalows")
.resizable()
.frame(width: 338, height: 200)
SwiftUI Preview of the image using both the .resizable() and .frame() modifiers. The image is resized, but it is skewed and has lost its aspect ratio.
SwiftUI Preview

It’s skewed! That’s not going to work. Let’s try to resize by aspect ratio.

// use aspect ratio + .fit to resize
Image("bungalows")
.resizable()
.aspectRatio(4/3, contentMode: .fit)
SwiftUI Preview with .resizable() and .aspectRatio() modifiers applied. The image is resized as expected, but the image is still skewed and did not maintain its aspect ratio.
SwiftUI Preview

Using .fit still gets us a skewed image. Instead, let's try using .fill

// use aspect ratio + .fill to resize
Image("bungalows")
.resizable()
.aspectRatio(4/3, contentMode: .fill)
SwiftUI Preview of the image using .fill for the .aspectRatio() modifier. The image is resized, but this time it fills the container and is still skewed, not maintaining its aspect ratio.
SwiftUI Preview

Well that went wrong. We’re going to need to try something different…or Rectangle.

// let the trickery begin
ZStack {
Rectangle()
.fill(Color(.gray))
.aspectRatio(4/3, contentMode: .fit)

Image("bungalows")
}
SwiftUI Preview of image stacked on top of a Rectangle. The image is displayed at its original size and the Rectangle is set to the desired size.
SwiftUI Preview

ZStack is how we stack things on top of each other in SwiftUI. The Rectangle is exactly the size we want our image to be. Can we make our image the same size as the Rectangle?

// getting closer...
ZStack {
Rectangle()
.fill(Color(.gray))
.aspectRatio(4/3, contentMode: .fit)

Image("bungalows")
.resizable()
.layoutPriority(-1) // special sauce
}
SwiftUI Preview of the image resized to the same size as the Rectangle(). The image, however, is distorted again.
SwiftUI Preview

We can! We’re giving .layoutPriority() to the Rectangle so that it is dictating the size instead of the Image. But now our image is skewed again! We need to fix that.

// Come on!!!
ZStack {
Rectangle()
.fill(Color(.gray))
.aspectRatio(4/3, contentMode: .fit)

Image("bungalows")
.resizable()
.aspectRatio(contentMode: .fill) // fix skewed image
.layoutPriority(-1)
}
SwiftUI Preview of the image using .fill. The image is no longer skewed, but now it is no longer the same size as our Rectangle. There is a blue outlined rectangle indicating where the Rectangle() is.
SwiftUI Preview

Note: the blue square is there to indicate where the Rectangle is. This wouldn’t be visible in the UI.

One last modifier and we’ve got this! We’re bringing .clipped() back!

// Yes!
ZStack {
Rectangle()
.fill(Color(.gray))
.aspectRatio(4/3, contentMode: .fit)

Image("bungalows")
.resizable()
.aspectRatio(contentMode: .fill)
.layoutPriority(-1)
}
.clipped() // magic
SwiftUI Preview of the image resized correctly and maintaining its aspect ratio.
SwiftUI Preview
Decorative separator

The Solution

Now that we finally have a working solution, we need to make this reusable so that we never have to go through this again.

Let’s convert this to a ViewModifier so that we can apply it to any Image.

A few things to note about the final solution:

  • We added an AspectRatio enum for the most common usages in the Vrbo iOS app. This solution supports both our predefined aspect ratios as well as a custom aspect ratio.
  • .resizable() was moved to the Image extension. .resizable() can only be applied to an Image, so it cannot be applied inside of the ViewModifier, which expects Content that could be any type of View.
  • .aspectRatio(contentMode: .fill) was replaced with its shortcut modifier, .scaleToFill()
  • We set the Rectangle fill color to be .clear instead of .gray. The only time the Rectangle would ever show would be if the image failed to render. For the Vrbo iOS app, we typically combine this modifier with other modifiers for providing a fallback state. However, this modifier could be expanded to support a fallback image or a fallback color.
Decorative separator

Usage

Image("resorts")
.fitToAspectRatio(.square)
SwiftUI Preview of image resized to a square. Image is of an outdoor pool at a luxury resort with white umbrellas, white beach lounge chairs and palm trees.
SwiftUI Preview of image resized to a square
Image("resorts")
.fitToAspectRatio(.fourToThree)
SwiftUI Preview of image resized to 4:3 aspect ratio. Image is of an outdoor pool at a luxury resort with white umbrellas, white beach lounge chairs and palm trees.
SwiftUI Preview of image resized to 4:3 aspect ratio
Image("resorts")
.fitToAspectRatio(.threeToFour)
SwiftUI Preview of image resized to 3:4 aspect ratio. Image is of an outdoor pool at a luxury resort with white umbrellas, white beach lounge chairs and palm trees.
SwiftUI Preview of image resized to 3:4 aspect ratio
Image("resorts")
.fitToAspectRatio(1.5)
.clipShape(RoundedRectangle(cornerRadius: 8))
SwiftUI Preview of image with rounded corners and resized to a custom 3:2 aspect ratio. Image is of an outdoor pool at a luxury resort with white umbrellas, white beach lounge chairs and palm trees.
SwiftUI Preview of image with rounded corners and resized to a custom 3:2 aspect ratio

Now, no matter the size of a container View, our image will always render to our desired aspect ratio without becoming distorted. Thanks for reading!

Learn more about technology at Expedia Group.

--

--