Pascal Bourque
Pascal Bourque

Reputation: 5299

How to make every item the same height in a LazyVGrid?

Edit This is a regression in iOS 15 beta. The code works as expected on iOS 14.5:

enter image description here

I have submitted a bug to Apple.


I have a dashboard-style screen in my SwiftUI app, where I am using a LazyVGrid with a single .adaptative column to layout my dashboard widgets, where widgets are laid out in wrapping rows.

It works as I want it to.

However, if a widget happens to be taller than others, I would like other widgets in the same row to grow vertically, so they end up having the same height as the tallest of the row.

This small bit of code illustrates my problem:

struct ContentView: View {
  var body: some View {
    LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {

      VStack {
        Spacer()
        Text("Hello")
      }
      .border(.red)

      Text("Lorem ipsum")
        .border(.blue)
    }
    .border(.green)
    .padding(.horizontal, 100)
  }
}

The result is:

screenshot

I would like the red box (VStack containing Spacer + Hello) to be as tall as the blue box (lorem ipsum).

How could I accomplish that?

Please don't suggest using an HStack, as the above example is only to illustrate my problem with LazyVGrid. I do need to use the grid because I have quite a few children to layout, and the grid works great between phone and iPad form factors (adjusting the number of columns dynamically, exactly as I want it).

Upvotes: 22

Views: 10353

Answers (4)

Fr3ak
Fr3ak

Reputation: 61

If Apple is using fixedSize to calculate the height of the row then putting .frame(maxHeight: .infinity) on each cell will fill the pre-calculated height.

Tried it on iOS 17 and it works.

Upvotes: 6

Radun C
Radun C

Reputation: 89

I had the same issue. What worked for me was to set the minHeight of Spacer to 0.

here is some dummy example code

struct MasterView: View {
    let columns = [
        GridItem(.flexible(), spacing: 12),
        GridItem(.flexible())
    ]
    
    var body: some View {
        VStack {
            LazyVGrid(columns: columns, spacing: 12) {
                ForEach(infoTexts, id:\.self) { infoText in
                    InfoFieldView(text: infoText)
                }
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }
}


struct InfoFieldView: View {
    var text: String
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text("Some text")
                .padding(.bottom, 4)
                 
            // Autoresizing does not work if you dont set minLenght
            Spacer(minLength: 0)
        }
    }
}

Upvotes: 0

Asperi
Asperi

Reputation: 257711

It looks like Apple begins (for unknown reason) to apply fixedSize for views in grid to make layout based on known intrinsic content sizes. The Spacer, Shape, Color, etc. do not have intrinsic size so we observe... that what's observed.

A possible approach to resolve this is perform calculations by ourselves (to find dynamically max height and apply it to all cells/views).

Here is a demo (with simple helper wrapper for cell). Tested with Xcode 13.2 / iOS 15.2

demo

struct ContentView: View {
    @State private var viewHeight = CGFloat.zero

    var body: some View {
        LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {

            GridCell(height: $viewHeight) {
                VStack {
                    Spacer()
                    Text("Hello")
                }
            }.border(.red)

            GridCell(height: $viewHeight) {
                Text("Lorem ipsum asdfd")
            }.border(.blue)
        }
        .onPreferenceChange(ViewHeightKey.self) {
            self.viewHeight = max($0, viewHeight)
        }
        .border(.green)
        .padding(.horizontal, 100)
    }
}

struct GridCell<Content: View>: View {
    @Binding var height: CGFloat
    @ViewBuilder let content: () -> Content

    var body: some View {
        content()
            .frame(minHeight: height)
            .background(GeometryReader {
                Color.clear.preference(key: ViewHeightKey.self,
                    value: $0.frame(in: .local).size.height)
            })
    }
}

struct ViewHeightKey: PreferenceKey {
    static var defaultValue: CGFloat { 0 }
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value += nextValue()
    }
}

Upvotes: 5

CZJ
CZJ

Reputation: 13

I had exactly same problem. My LazyVGrid looked great on iOS 14, but now its items have different heights.

I found a dirty workaround to force the items have same height: In my case I have only several items in each LazyVGrid (So it won't cause too much performance drop), and it is easy for me to know which item has the largest height. So I made a ZStack and put a transparent highest item behind the actual item.

struct ContentView: View {
  var body: some View {
    LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
      ZStack {
        Text("Lorem ipsum")  // This is the highest item.
          .opacity(0)        // Make it transparent.
        Text("Hello")
      }
      .border(.red)

      Text("Lorem ipsum")
        .border(.blue)
    }
    .border(.green)
    .padding(.horizontal, 100)
  }
}

This workaround works in my case, but I don't recommend using it widely in your app especially when you have a lot of items in the grid.

Upvotes: 0

Related Questions