5

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!

Robert Dresler
  • 10,580
  • 2
  • 22
  • 40
  • I had a similar problem and I ended up using an HStack which calculated all heights via preference key in advance and applied the bigger float height to all views. Not really sure though how you can achieve that by going lazy. – snksnk Oct 07 '22 at 08:02
  • You can probably try something with `Layout` but I don't think it would be lazy – lorem ipsum Oct 10 '22 at 17:26

1 Answers1

0

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.

  • Hey, you explained what I already explained in my question, so this isn't answer which would answer my question. My behaviour is to switch it based on visible content as I described. – Robert Dresler Oct 10 '22 at 10:48
  • Sorry, I just wanted to clarify the requirements. So the effect you expect is the smoothness of size switching? – LIN SUNG HAO Oct 11 '22 at 04:53
  • Effect I expect is described in the question. I need solution which is not laggy and is better than my hard calculating. – Robert Dresler Oct 11 '22 at 09:06