Luca
Luca

Reputation: 313

Clipping a resized image using aspectRatio scaling SwiftUI

At the end of the day, I'm trying to scale to fill an image into a height of X (439px in my case) and then clip it using a RoundedRectangle.

post.picture
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(height: 439)
    .clipped()
    .clipShape(Circle())

This view is inside a VStack. If that is necessary to resolve this, I'll post it too, but I don't think it is.

post.picture is an Image property within the Post struct. It looks like this

struct Post: Hashable, Codable {
    var id: Int
    var userId: Int
    var location: String
    var activityId: Int
    var cheerCount: Int
    
    private var pictureLocation: String
    var picture: Image {
        let mainPath = "/hardcoded/path/"
        if let image = UIImage(contentsOfFile: "\(mainPath)/\(pictureLocation)") {
            return Image(uiImage: image)
        } else {
            return Image(systemName: "photo")
        }
    }
}

The code produces this

enter image description here

The issue is that the circle is cut off at the edges. I want the circle mask to be scaled down but the image to retain it's aspect ratio and size.

I am not sure how to do this. I'm using a clipShape(Circle()) to make the issue more clear but I really want to clip the image with RoundedRectangle.

The code works properly when the image height is larger than width, and cuts off the mask shape when the width larger than the height.

It's suppose to look like this

enter image description here

Upvotes: 1

Views: 2776

Answers (3)

Eric Yuan
Eric Yuan

Reputation: 1512

Best avoid using UIScreen.main.bounds.size.width for setting view dimensions due to two reasons:

  1. The use of main will be deprecated in future iOS versions.
  2. It represents a static value that does not adapt to device orientation changes, such as rotating.

In SwiftUI, parent views do not inherently dictate the size and position of child views. This becomes particularly relevant when dealing with images set to scaledToFill. This setting causes an image to expand until one of its dimensions aligns with the corresponding dimension of its parent view. However, without explicit constraints, this can lead to the image exceeding the bounds of the parent view if the other dimension is smaller. I think this is the biggest difference from image behaviours in UIKit.

So, we'd better explicitly define the dimensions of images. This ensures that they do not unexpectedly exceed the parent view's bounds. Here, the GeometryReader plays a role by providing access to the parent view's size, which reflects the available screen area for the image. This allows for dynamic sizing of the image based on the device's screen dimensions or the parent view's size.

Consider the following code snippet:

GeometryReader { geometry in
    post.picture
        .resizable()
        .scaledToFill()
        .frame(width: geometry.size.width, height: 439)
        .clipped()
        .cornerRadius(10)
}
// Apply padding or other modifiers below this line. As GeometryReader acts as a container view and may affect layout.

Upvotes: 1

Luca
Luca

Reputation: 313

Okay, I figured it out.

The frame(height: 439) and .fill caused the internal width value of the frame to be extended past the screen limits in order to keep the aspect ratio when the width > height.

This made it so the left and right edges of the frame were cut off when width > height. This seems like silly behaviour to me since there is nothing to indicate that the frame is larger than the screen width. Visually, the image looks clipped to the correct size, and documentation wise would imply this should work properly without explicitly setting the frame size (given that the max width of the VStack should be the width of the screen).

To fix it, I needed to place an explicit width size on the frame

post.picture
    .resizable()
    .scaledToFill()
    .frame(width: UIScreen.main.bounds.size.width, height: 439)
    .clipped()
    .cornerRadius(10)

.infinity doesn't work on the width like @Fuad suggested because that would only scale the frame to it's already large width. The frame needed to be cropped essentially.

Upvotes: 0

Fuad
Fuad

Reputation: 448

Please try this solution:

post.picture
    .resizable()
    .scaledToFit()
    .frame(
        minWidth: 0,
        maxWidth: .infinity,
        minHeight: 439,
        maxHeight: 439
    )
    .clipped()
    .clipShape(Circle())

Upvotes: 0

Related Questions