Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clip image to square in SwiftUI

Tags:

ios

swiftui

I am trying to put multiple cells next to each other where each cell consists of an image and a text below. The cell itself should be a square and the image should be scaled to fill the remaining space (cutting a part of the image).

First I tried just making the image square and the text below. Now my problem is, that I don't know how to properly achieve that in SwiftUI. I can get it to work, when using this code:

VStack {
    Image(uiImage: recipe.image!)
        .resizable()
        .aspectRatio(contentMode: .fill)
        .frame(width: 200, height: 200, alignment: .center)
        .clipped()
    Text(recipe.name)
}

Image 1 The problem is, that I have to specify a fixed frame size. What I want is a way to make a cell, that keeps an aspect ratio of 1:1 and is resizable, so I can fit a dynamic amount of them on a screen next to each other.

I also tried using

VStack {
    Image(uiImage: recipe.image!)
        .resizable()
        .aspectRatio(1.0, contentMode: .fit)
        .clipped()
    Text(recipe.name)
}

Image 2 which gives me square images, that scale dynamically. But the problem is, that the image now gets stretched to fill the square and not scaled to fill it.

My next idea was to clip it to a square shape. For that I first tried to clip it into a circle shape (because apparently there is not square shape):

VStack {
    Image(uiImage: recipe.image!)
        .resizable()
        .aspectRatio(contentMode: .fit)
        .clipShape(Circle())
    Text(recipe.name)
}

Image 3

But for some odd reason, it didn't really clip the image but instead kept the remaining space...

So am I not seeing something or is the only option to clip an image square the frame modifier?

EDIT

To clarify: I don't care about the text as much, as about the whole cell (or if it's simpler the image) being square, without having to specify its size via .frame and without the non-square original image being stretched to make it fit.

So the perfect solution would be that the VStack is square but getting a square image would also be okay. It should look like Image 1, but without having to use the .frame modifier.

like image 691
iComputerfreak Avatar asked Oct 08 '19 17:10

iComputerfreak


People also ask

How do I fit an image in SwiftUI?

To make an image scales to fit the current view, we use the resizable() modifier, which resizes an image to fit available space. We can see the whole image now, but it got stretched out and losing its aspect ratio, which is usually not the behavior we want. The image view resize to fit available space.

How do I clip a view in SwiftUI?

SwiftUI lets you clip any view to control its shape, all by using the clipShape() modifier. The Circle clip shape will always make circles from views, even if their width and height are unequal – it will just crop the larger value to match the small.

How do I round an image in SwiftUI?

Any SwiftUI view can have its corners rounded using the cornerRadius() modifier. This takes a simple value in points that controls how pronounced the rounding should be.

How do I center a crop image in SwiftUI?

To use the Image extension , just put it in a file in your project (a name like image-centercropped. swift will work nicely). Then just add . centerCropped() to any image you want to be center cropped.


2 Answers

A ZStack will help solve this by allowing us to layer views without one effecting the layout of the other.

For the text:

.frame(minWidth: 0, maxWidth: .infinity) to expand the text horizontally to its parent's size

.frame(minHeight: 0, maxHeight: .infinity) is useful in other situations

As for the image:

.aspectRatio(contentMode: .fill) to make the image maintain its aspect ratio rather than squashing to the size of its frame.

.layoutPriority(-1) to de-prioritize laying out the image to prevent it from expanding its parent (the ZStack within the ForEach in our case).

The value for layoutPriority just needs to be lower than the parent views which will be set to 0 by default. We have to do this because SwiftUI will layout a child before its parent, and the parent has to deal with the child size unless we manually prioritize differently.

The .clipped() modifier uses the bounding frame to mask the view so you'll need to set it to clip any images that aren't already 1:1 aspect ratio.

    var body: some View {
        HStack {
            ForEach(0..<3, id: \.self) { index in
                ZStack {
                    Image(systemName: "doc.plaintext")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .layoutPriority(-1)
                    VStack {
                        Spacer()
                        Text("yes")
                            .frame(minWidth: 0, maxWidth: .infinity)
                            .background(Color.white)
                    }
                }
                .clipped()
                .aspectRatio(1, contentMode: .fit)
                .border(Color.red)
            }
        }
    }

Edit: While geometry readers are super useful I think they should be avoided whenever possible. It's cleaner to let SwiftUI do the work. This is my initial solution with a Geometry Reader that works just as well.

        HStack {
            ForEach(0..<3, id: \.self) { index in
                ZStack {
                    GeometryReader { proxy in
                        Image(systemName: "pencil")
                            .resizable()
                            .scaledToFill()
                            .frame(width: proxy.size.width)
                        VStack {
                            Spacer()
                            Text("yes")
                                .frame(width: proxy.size.width)
                                .background(Color.white)
                        }
                    }
                }
                .clipped()
                .aspectRatio(1, contentMode: .fit)
                .border(Color.red)
            }
        }

like image 56
ethoooo Avatar answered Sep 24 '22 03:09

ethoooo


It works for me, but I don't know why cornerRadius is necessary...

import SwiftUI

struct ClippedImage: View {
    let imageName: String
    let width: CGFloat
    let height: CGFloat

    init(_ imageName: String, width: CGFloat, height: CGFloat) {
        self.imageName = imageName
        self.width = width
        self.height = height
    }
    var body: some View {
        ZStack {
            Image(imageName)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: width, height: height)
        }
        .cornerRadius(0) // Necessary for working
        .frame(width: width, height: height)
    }
}

struct ClippedImage_Previews: PreviewProvider {
    static var previews: some View {
        ClippedImage("dishLarge1", width: 100, height: 100)
    }
}

enter image description here

like image 35
kazuwombat Avatar answered Sep 20 '22 03:09

kazuwombat