Home > Software design >  How to make LazyVGrid GridItems all the same height?
How to make LazyVGrid GridItems all the same height?

Time:01-03

I have a LazyVGrid inside a NavigationView that consists of a 2 x N grid of items. Each item contains some text of varying length and I want them to be all the same height as the biggest item.

Here's a version of my code that can be copied and pasted into a SwiftUI Playground:

import SwiftUI
import PlaygroundSupport

struct TileGridView: View {
  private var items: [GridItem] {
    Array(
      repeating: GridItem(
        .adaptive(minimum: 150),
        spacing: 10
      ),
      count: 2
    )
  }

  var body: some View {
    NavigationView {
      LazyVGrid(columns: items, spacing: 10) {
        TileCellView(
          text: "Lorem Ipsum"
        )
        TileCellView(
          text: "Lorem Ipsum Dolem"
        )
        TileCellView(
          text: "Lorem ipsum dolor sit amet"
        )
        TileCellView(
          text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit,"
        )
        TileCellView(
          text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
       )
      }
      .padding(.horizontal)
      .navigationTitle("")
      .navigationBarTitleDisplayMode(.inline)
      .toolbar {
        ToolbarItem(placement: .navigationBarLeading) {
          Image(systemName: "checkmark")
        }
        ToolbarItem(placement: .navigationBarTrailing) {
          Image(systemName: "checkmark")
        }
      }
    }.navigationViewStyle(.stack)
  }
}


struct TileCellView: View {
  @State private var isSelected: Bool = false
  let text: String

  var body: some View {
    ZStack {
      Text(text)
        .padding()
        .frame(maxWidth: .infinity)
        .background(
          RoundedRectangle(cornerRadius: 25, style: .continuous)
            .foregroundColor(.blue)
        )
        .contentShape(Rectangle())
        .onTapGesture {
          isSelected = true
        }

      NavigationLink(
        isActive: $isSelected,
        destination: {
          Text("hi")
        },
        label: {
          EmptyView()
        }
      ).hidden()
    }
  }
}

PlaygroundPage.current.setLiveView(TileGridView())

Now I tried to add a PreferenceKey to TileGridView to find the frame height of the biggest item but I couldn't get it working.

UPDATE: Here's my code with a preference key involved:

struct TileGridView: View {
  @State private var priceHeight: CGFloat?
  private var items: [GridItem] {
    Array(
      repeating: GridItem(
        .adaptive(minimum: 150),
        spacing: 10
      ),
      count: 2
    )
  }

  var body: some View {
    NavigationView {
      LazyVGrid(columns: items, spacing: 10) {
        Group {
          TileCellView(
            text: "Lorem Ipsum",
            height: $priceHeight
          )
          TileCellView(
            text: "Lorem Ipsum Dolem",
            height: $priceHeight
          )
          TileCellView(
            text: "Lorem ipsum dolor sit amet",
            height: $priceHeight
          )
          TileCellView(
            text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit,",
            height: $priceHeight
          )
          TileCellView(
            text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor",
            height: $priceHeight
         )
        }
        .background(
          GeometryReader { geometry in
            Color.clear.preference(
              key: HeightPreferenceKey.self,
              value: geometry.size.height
            )
          }
        )
      }

      .onPreferenceChange(HeightPreferenceKey.self) {
        priceHeight = $0
      }
      .padding(.horizontal)
      .navigationTitle("")
      .navigationBarTitleDisplayMode(.inline)
      .toolbar {
        ToolbarItem(placement: .navigationBarLeading) {
          Image(systemName: "checkmark")
        }
        ToolbarItem(placement: .navigationBarTrailing) {
          Image(systemName: "checkmark")
        }
      }
    }.navigationViewStyle(.stack)
  }
}

private extension TileGridView {
  struct HeightPreferenceKey: PreferenceKey {
    static let defaultValue: CGFloat = 0

    static func reduce(
      value: inout CGFloat,
      nextValue: () -> CGFloat
    ) {
      value = max(value, nextValue())
    }
  }
}


struct TileCellView: View {
  @State private var isSelected: Bool = false
  let text: String
  @Binding var height: CGFloat?

  var body: some View {
    ZStack {
      Text(text)
        .padding()
        .frame(maxWidth: .infinity)
        .frame(height: height)
        .background(
          RoundedRectangle(cornerRadius: 25, style: .continuous)
            .foregroundColor(.blue)
        )
        .contentShape(Rectangle())
        .onTapGesture {
          isSelected = true
        }

      NavigationLink(
        isActive: $isSelected,
        destination: {
          Text("hi")
        },
        label: {
          EmptyView()
        }
      ).hidden()
    }
  }
}

Does anyone know how to make this work? Thanks!

CodePudding user response:

The issue that you are having is twofold: 1. you are reading the size of the entire LazyVGrid, not the individual cells, 2. you can't set the frame height prior to reading the cells with the PreferenceKey. What the PreferenceKey does is it reads the height that the cell wants to be before is is contained by the frame. By then taking the largest of those, and setting the frame height to that, we make them all the same size, and just large enough to contain the text. The last thing I had to do is move the blue background out of the cells as that has to be applied after the .frame(). I also cut out that part of the code that didn't relate to the LazyVGrid' or PreferenceKey` for clarity and conciseness. Your extension is unchanged. So, you get this:

struct TileGridView: View {
    @State private var priceHeight: CGFloat = 50
    
    private var items: [GridItem] {
        Array(
            repeating: GridItem(
                .adaptive(minimum: 150),
                spacing: 10
            ),
            count: 2
        )
    }
    
    var body: some View {
        NavigationView {
            LazyVGrid(columns: items, spacing: 10) {
                Group {
                    TileCellView(text: "Lorem Ipsum")
                        .background(
                            GeometryReader { geometry in
                                Color.clear.preference(
                                    key: HeightPreferenceKey.self,
                                    value: geometry.size.height
                                )
                            }
                        )
                        .frame(height: priceHeight)
                        .background(
                            RoundedRectangle(cornerRadius: 25, style: .continuous)
                                .foregroundColor(.blue)
                        )
                    TileCellView(text: "Lorem Ipsum Dolem")
                        .background(
                            GeometryReader { geometry in
                                Color.clear.preference(
                                    key: HeightPreferenceKey.self,
                                    value: geometry.size.height
                                )
                            }
                        )
                        .frame(height: priceHeight)
                        .background(
                            RoundedRectangle(cornerRadius: 25, style: .continuous)
                                .foregroundColor(.blue)
                        )
                    TileCellView(text: "Lorem ipsum dolor sit amet")
                        .background(
                            GeometryReader { geometry in
                                Color.clear.preference(
                                    key: HeightPreferenceKey.self,
                                    value: geometry.size.height
                                )
                            }
                        )
                        .frame(height: priceHeight)
                        .background(
                            RoundedRectangle(cornerRadius: 25, style: .continuous)
                                .foregroundColor(.blue)
                        )
                    TileCellView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit,")
                        .background(
                            GeometryReader { geometry in
                                Color.clear.preference(
                                    key: HeightPreferenceKey.self,
                                    value: geometry.size.height
                                )
                            }
                        )
                        .frame(height: priceHeight)
                        .background(
                            RoundedRectangle(cornerRadius: 25, style: .continuous)
                                .foregroundColor(.blue)
                        )
                    TileCellView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor")
                        .background(
                            GeometryReader { geometry in
                                Color.clear.preference(
                                    key: HeightPreferenceKey.self,
                                    value: geometry.size.height
                                )
                            }
                        )
                        .background(
                            RoundedRectangle(cornerRadius: 25, style: .continuous)
                                .foregroundColor(.blue)
                        )
                }
            }
            
            .onPreferenceChange(HeightPreferenceKey.self) {
                priceHeight = $0
            }
            .padding(.horizontal)
        }.navigationViewStyle(.stack)
    }
}

struct TileCellView: View {
    let text: String
    
    var body: some View {
        ZStack {
            Text(text)
                .padding()
                .frame(maxWidth: .infinity)
                .contentShape(Rectangle())
        }
    }
}
  • Related