3

Items inside a SwiftUI LazyVGrid shrink to their minWidth (80 points in my example below) when the grid view expands enough to fit another column. This is visually expected when there are multiple rows:

enter image description here

However, when there's only one row, this is visually clumsy (in my opinion):

enter image description here

Instead, when there's only one row, I would like the items to expand all the way up to their maxWidth and then stay there:

enter image description here

I achieved the last result by hardcoding a maxWidth on the LazyVGrid but:

  1. I'm not sure how to programmatically calculate what the grid's maxWidth would need to be and
  2. the items never reach their maxWidth (in my example that would be 150 points) because the current behaviour shrinks them repeatedly as the available space grows.

You can see this exact problem in the swiftUI macOS Shortcuts app.

The better, desired behaviour can be seen in:

  • Books (Library > All)
  • Notes ("Gallery" view)
  • Music
  • Photos etc

The pertinent parts of my code:

    let items = ... /// My items here

    let gridItems = [
        GridItem(.adaptive(minimum: 80))
    ]

    var body: some View {
        VStack(alignment: .leading) {
            Text("LazyVGrid:")
        
            HStack {
                LazyVGrid(columns: gridItems) {
                    ForEach(items, id: \.self) { item in
                        itemView
                    }
                }
                .frame(maxWidth: 510) /// For the final sample above
                Spacer()
            }
            Spacer()
        }
    }
    
    private var itemView: some View {
        GeometryReader { geo in
            HStack {
                VStack {                
                    Text("\(Int(geo.size.width))")
                }
            }
        }
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
    }
jeff-h
  • 2,184
  • 1
  • 22
  • 33
  • It happens due to used GeometryReader and .infinity width in item view, so all items are recalculated on any parent frame changes. LazyVGrid actually expect fixed-sized views here to place into flexible space, but here both things are flexible, so I assume we observe just some result of internal breaking cycling layout calculations conflict. – Asperi Jul 12 '22 at 08:54
  • Sorry if I'm misunderstanding you, but the same problem occurs without the GeometryReader wrapper, and also if I move .infinity maxWidth from the items to the grid. Is there some way to know when to switch something at a certain width to avoid the parent frame changes having an effect? – jeff-h Jul 12 '22 at 10:38

1 Answers1

0

So I had been hoping either a) I was overlooking something simple or b) it would be possible with the right combination of modifiers.

Since that's not looking to be the case, I went ahead and built my own solution, which basically flips from a LazyVGrid to an HStack when there's only one row. There are some caveats, but otherwise it works just fine.

  • you have to provide a maxWidth on each item
  • it won't work if you want columns with different widths

Not totally sure it's worth the extra lines of code just for this effect but nonetheless, it may be useful for someone:

@available(macOS 11.0, *)
public struct LazyNiceVGrid<Content: View>: View {

    private let itemCount: Int
    private let minItemWidth: CGFloat
    private let spacing: CGFloat
    private let content: Content

    public init(itemCount: Int, minItemWidth: CGFloat, spacing: CGFloat = 8, @ViewBuilder content: () -> Content) {
        self.itemCount = itemCount
        self.minItemWidth = minItemWidth
        self.spacing = spacing
        self.content = content()
    }
    
    public var body: some View {
        GeometryReader { geo in
            let itemCount = CGFloat(itemCount)
            let itemWidth = itemCount * minItemWidth
            let spacerWidth = (itemCount - 1) * spacing
            if geo.size.width < (itemWidth + spacerWidth) {
                let columns = [
                    GridItem(.adaptive(minimum: minItemWidth, maximum: .infinity), spacing: spacing)
                ]
                LazyVGrid(columns: columns, spacing: spacing) {
                    content
                }
            }
            else {
                HStack(spacing: spacing) {
                    content
                }
            }
            Spacer()
        }
    }
}

@available(macOS 11.0, *)
struct LazyNiceVGrid_Previews: PreviewProvider {
    static var previews: some View {
        LazyNiceVGrid(itemCount: 4, minItemWidth: 120) {
            Group {
                Text("one")
                Text("two")
                Text("three")
                Text("four")
            }
            .frame(minWidth: 0, maxWidth: 250, minHeight: 50)
            .background(Color.blue)
        }
    }
}

You'd call it something like in the PreviewProvider above, or:

LazyNiceVGrid(itemCount: items.count, minItemWidth: 120) {
  ForEach(items, id: \.self) { item in
    itemView
      .frame(minWidth: 0, maxWidth: 250, minHeight: 50)
  }
}
jeff-h
  • 2,184
  • 1
  • 22
  • 33