SwiftUI - Dynamic LazyHGrid row height

2.3k Views Asked by At

I'm creating vertical layout which has scrollable horizontal LazyHGrid in it. The problem is that views in LazyHGrid can have different heights (primarly because of dynamic text lines) but the grid always calculates height of itself based on first element in grid:

enter image description here

What I want is changing size of that light red rectangle based on visible items, so when there are smaller items visible it should look like this:

enter image description here

and when there are bigger items it should look like this:

enter image description here

This is code which results in state on the first image:

struct TestView: PreviewProvider {

    static var previews: some View {
        ScrollView {
            VStack {
                Color.blue
                    .frame(height: 100)
                ScrollView(.horizontal) {
                    LazyHGrid(
                        rows: [GridItem()],
                        alignment: .top,
                        spacing: 16
                    ) {
                        Color.red
                            .frame(width: 64, height: 24)
                        ForEach(Array(0...10), id: \.self) { value in
                            Color.red
                                .frame(width: 64, height: CGFloat.random(in: 32...92))
                        }
                    }.padding()
                }.background(Color.red.opacity(0.3))
                Color.green
                    .frame(height: 100)
            }
        }
    }
}

Something similar what I want can be achieved by this:

extension View {
    func readSize(edgesIgnoringSafeArea: Edge.Set = [], onChange: @escaping (CGSize) -> Void) -> some View {
        background(
            GeometryReader { geometryProxy in
                SwiftUI.Color.clear
                    .preference(key: ReadSizePreferenceKey.self, value: geometryProxy.size)
            }.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
        )
        .onPreferenceChange(ReadSizePreferenceKey.self) { size in
            DispatchQueue.main.async { onChange(size) }
        }
    }
}

struct ReadSizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}

struct Size: Equatable {
    var height: CGFloat
    var isValid: Bool
}

struct TestView: View {

    @State private var sizes = [Int: Size]()
    @State private var height: CGFloat = 32
    static let values: [(Int, CGFloat)] =
        (0...3).map { ($0, CGFloat(32)) }
        + (4...10).map { ($0, CGFloat(92)) }

    var body: some View {
        ScrollView {
            VStack {
                Color.blue
                    .frame(height: 100)
                ScrollView(.horizontal) {
                    LazyHGrid(
                        rows: [GridItem(.fixed(height))],
                        alignment: .top,
                        spacing: 16
                    ) {
                        ForEach(Array(Self.values), id: \.0) { value in
                            Color.red
                                .frame(width: 300, height: value.1)
                                .readSize { sizes[value.0]?.height = $0.height }
                                .onAppear {
                                    if sizes[value.0] == nil {
                                        sizes[value.0] = Size(height: .zero, isValid: true)
                                    } else {
                                        sizes[value.0]?.isValid = true
                                    }
                                }
                                .onDisappear { sizes[value.0]?.isValid = false }
                        }
                    }.padding()
                }.background(Color.red.opacity(0.3))
                Color.green
                    .frame(height: 100)
            }
        }.onChange(of: sizes) { sizes in
            height = sizes.filter { $0.1.isValid }.map { $0.1.height }.max() ?? 32
        }
    }

}

enter image description here

... but as you see its kind of laggy and a little bit complicated, isn't there better solution? Thank you everyone!

1

There are 1 best solutions below

3
On

The height of a row in a LazyHGrid is driven by the height of the tallest cell. According to the example you provided, the data source will only show a smaller height if it has only a small size at the beginning.
Unless the first rendering will know that there are different heights, use the larger value as the height.

Is your expected UI behaviour that the height will automatically switch? Or use the highest height from the start.