11

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

DanubePM
  • 1,526
  • 1
  • 10
  • 24
  • Image has own size, but Shape, in this case Rectangle, does not, so layout might differ (with same input parameters). Would you show somehow what do you expect to get for images? – Asperi Jul 08 '20 at 11:02
  • I've updated my question with an example of what I want. – DanubePM Jul 08 '20 at 18:03

8 Answers8

19

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)
    }
}
flainez
  • 11,797
  • 1
  • 18
  • 16
DanubePM
  • 1,526
  • 1
  • 10
  • 24
  • This solved this same issue for me but with changing the frame modifier to .frame(height: gr.size.width) so that my image would always have the same width and height. I'm curious what setting the frame's width to the geometry's proposed width does here. – Jake Strickler Aug 23 '20 at 02:43
  • 1
    is it me or does this not center the image in the middle of the square though? – chopsalot Oct 07 '20 at 19:40
  • Try this frame modifier:frame(height: gr.size.width, alignment: .center) – William Choy Sep 11 '21 at 07:25
12

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

chopsalot
  • 334
  • 4
  • 10
7

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)
    }
}
JoshHolme
  • 303
  • 2
  • 15
3

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
            }
        })
    }
}
Murlakatam
  • 2,729
  • 2
  • 26
  • 20
3

The question is very close to the easiest solution! This doesn't use GeometryReader or frame:

LazyVGrid(columns: threeColumnGrid, alignment: .center) {
    ForEach(model.imageNames, id: \.self) { imageName in
          Rectangle()
            .aspectRatio(1, contentMode: .fill)
            .overlay {
              Image(imageName)
                .resizable()
                .scaledToFill()
            }
            .cornerRadius(8)
            .clipped()
    }
}
Gigisommo
  • 1,232
  • 9
  • 9
0

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)
        )
}
Justin Sato
  • 523
  • 1
  • 4
  • 20
0

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()
                    }
                }
            }
    }
}
MtY.Hao
  • 1
  • 1
0

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.

Sajjad Sarkoobi
  • 827
  • 8
  • 18