How do you clip an image to a square inside a lazy grid? (SwiftUI)

Viewed 3937

This is not a duplicate of this question.

Below you'll see a grid of rectangles with 3 columns. I'm trying to achieve the same effect but with images. The images should be clipped, but otherwise not appear stretched or distorted.

Here's how I achieved the rectangle grid...

// In my view struct...

private let threeColumnGrid = [
    GridItem(.flexible(minimum: 40)),
    GridItem(.flexible(minimum: 40)),
    GridItem(.flexible(minimum: 40)),
]

// In my body...

LazyVGrid(columns: threeColumnGrid, alignment: .center) {
    ForEach(model.imageNames, id: \.self) { imageName in
        Rectangle()
            .foregroundColor(.red)
            .aspectRatio(1, contentMode: .fit)
    }
}

This is the layout I want using Rectangles...

Rectangle Grid Image

This is my goal...

Photos App Screenshot

Update:

If I do this...

Image(item)
    .resizable()
    .aspectRatio(1, contentMode: .fit)

The images distort if their aspect ratio wasn't already 1:1. For example, the circle image in the screenshot below should be a perfect circle.

Failed Example 1

7 Answers

I found a solution.

Defining the number of columns...

// Somewhere in my view struct
private let threeColumnGrid = [
    GridItem(.flexible(minimum: 40)),
    GridItem(.flexible(minimum: 40)),
    GridItem(.flexible(minimum: 40)),
]

Creating the grid...

LazyVGrid(columns: threeColumnGrid, alignment: .center) {
    ForEach(model.imageNames, id: \.self) { item in
        GeometryReader { gr in
            Image(item)
                .resizable()
                .scaledToFill()
                .frame(height: gr.size.width)
        }
        .clipped()
        .aspectRatio(1, contentMode: .fit)
    }
}

I found the DPMitu's answer was not centering the image (it was aligned to the leading edge).

This worked better for me (which also doesn't require GeometryReader):

  LazyVGrid(columns: columns, spacing: 30) {
       ForEach(items) { item in
           Image("IMG_0044")
               .resizable()
               .scaledToFill()
               .frame(minWidth: 0, maxWidth: .infinity)
               .aspectRatio(1, contentMode: .fill)
               .clipped() //Alternatively you can use cornerRadius for the same effect
               //.cornerRadius(10)
       }
  }
    

I found it here https://www.appcoda.com/learnswiftui/swiftui-gridlayout.html about half way down the page

Some were asking about the image appearing off center. You can adjust that by adding in the line .position(x: gr.frame(in: .local).midX, y: gr.frame(in: .local).midY). So the full thing would be:

Number of Columns

// Somewhere in my view struct
private let threeColumnGrid = [
    GridItem(.flexible(minimum: 40)),
    GridItem(.flexible(minimum: 40)),
    GridItem(.flexible(minimum: 40)),
]

Creating the Grid

LazyVGrid(columns: threeColumnGrid, alignment: .center) {
    ForEach(model.imageNames, id: \.self) { item in
        GeometryReader { gr in
            Image(item)
                .resizable()
                .scaledToFill()
                .frame(height: gr.size.width)
                .position(x: gr.frame(in: .local).midX, y: gr.frame(in: .local).midY)
        }
        .clipped()
        .aspectRatio(1, contentMode: .fit)
    }
}

Solution without GeometryReader and images that scale correctly.

struct ContentView: View {

    private let columns = [
        GridItem(.flexible(minimum: 40)),
        GridItem(.flexible(minimum: 40)),
        GridItem(.flexible(minimum: 40)),
    ]

    private let imageNames = ["image0", "image1", "image2", "image3", "image4"];

    var body: some View {
        LazyVGrid(columns: columns, content: {
            ForEach(imageNames, id: \.self) { name in
                Color.clear
                    .background(Image(name)
                                    .resizable()
                                    .scaledToFill()
                    )
                    .aspectRatio(1, contentMode: .fill)
                    .clipped()
                    .contentShape(Rectangle()) // <-- this is important to fix gesture recognition
            }
        })
    }
}

I faced a similar problem and this is how I solved it.

GeometryReader { geo in
    Image(item)
        .resizable()
        .aspectRatio(contentMode: .fill)
        .clipShape(
            Rectangle()
                .size(width: geo.size.width, height: geo.size.width)
        )
}

I implement using Rectangle for help, just below

LazyVGrid(columns: [.init(.adaptive(minimum: 88, maximum: 128), spacing: 2)], spacing: 2) {
    ForEach(urls, id: \.self) { url in
        Rectangle()
            .aspectRatio(1, contentMode: .fit)
            .foregroundColor(.clear)
            .overlay {
                GeometryReader { geometry in
                    if let image = UIImage(contentsOfFile: url.relativePath) {
                        Image(uiImage: image)
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                            .clipped()
                    }
                }
            }
    }
}

There are 2 ways to achieve this:

1. By using GeometryReader

    var gridView: some View {
        LazyVGrid(columns: [GridItem(.flexible(), spacing: 2, alignment: nil),
                            GridItem(.flexible(), spacing: 2, alignment: nil),
                            GridItem(.flexible(), spacing: 2, alignment: nil)],
                  alignment: .center, spacing: 2) {
            
            ForEach(0..<20) { _ in
                RoundedRectangle(cornerRadius: 0)
                    .aspectRatio(1.0 , contentMode: .fill)
                    .foregroundColor(.gray.opacity(0.3))
                    .overlay {
                        GeometryReader { proxy in
                            Image("iphone")
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                                .frame(width: proxy.size.width, height: proxy.size.width, alignment: .center)
                                .clipped()
                        }
                    }
            }
        }
    }

2. By setting frames:

    var gridView: some View {
        LazyVGrid(columns: [GridItem(.flexible(), spacing: 2, alignment: nil),
                            GridItem(.flexible(), spacing: 2, alignment: nil),
                            GridItem(.flexible(), spacing: 2, alignment: nil)],
                  alignment: .center, spacing: 2) {
            
            ForEach(0..<20) { _ in
                RoundedRectangle(cornerRadius: 0)
                    .aspectRatio(1.0 , contentMode: .fill)
                    .foregroundColor(.gray.opacity(0.3))
                    .overlay {
                        Image("iphone")
                            .resizable()
                            .scaledToFill()
                            .frame(minWidth: 0, maxWidth: .infinity)
                            .frame(minHeight: 0, maxHeight: .infinity)
                            .aspectRatio(1, contentMode: .fill)
                            .clipped()
                    }
            }
        }
    }

I'm suggesting not using GeometryReader it has some overload. Especially when loading a long list. It has to calculate each item's size.

Related