Search code examples
buttonscrollswiftuilazyvgrid

Why does putting a view in a Button result in terrible lag on LazyVGrid (bug?)?


Here is my code. Without the button, but with an onTapGesture, the scrolling is buttery smooth. Great! But if I put the Text() view in a button, then the lag is really, really bad, hanging even a bit...

This is simplified code. In my actual project, I have a view called GridCell, which is essentially a coloured box with a string inside it. But if I put that view in a button (rather than using button in the ForEach with GridCell as its content / label), the lag is also there. Should we avoid buttons in LazyVGrids or is this a bug of some sort?

    import SwiftUI

    let columnCount: Int = 11
    let gridSpacing: CGFloat = 1

    struct SimpleGridView: View {
        
        @State private var selected: String? = nil
        
        let data = (1...1000).map { "\($0)" }
        let columns: [GridItem] = Array(repeating: .init(.flexible(), spacing: gridSpacing), count: columnCount)
        let colCount: CGFloat = CGFloat(columnCount)
        
        var body: some View {
            GeometryReader { geo in
                ScrollView (showsIndicators: false) {
                    LazyVGrid(columns: columns, spacing: gridSpacing) {
                        ForEach(data, id: \.self) { item in
                            
                            // This code creates lag when scrolling
                            
//                                                    Button(action: {
//                                                        selected = item
//                                                    }) {
//                                                        Text(item)
//                                                    }
                            
                            /// This code is fine, apparently.
                            Text(item)
                                .onTapGesture(count: 1, perform: {
                                    selected = item
                                })
                        }
                    }
                    .sheet(item: $selected) { item in     // activated on selected item
                        DetailView(item: item)
                    }
                    .padding(.horizontal)
                }
            }
        }
    }

    struct DetailView: View {
        let item: String
        var body: some View {
            Text(item)
        }
    }

Solution

  • This looks like a SwiftUI limitation. I had the same issue and I found a workaround for it. I tried to investigate with the time profiler what was the root problem, but my investigation ended up in what looks like a caching issue within SwiftUI. 40% of the computational time was used by HVGrid.lengthAndSpacing to access the cache. Based on its name I think that it is used internally by the VGrid to compute each row size, but for some reason, it doesn't work well with buttons.

    enter image description here

    I tried to use the button in an overlay. In this way, the size of a single "cell" is calculated based on the view and then the button takes its size from the view. It seems to work.

    So the code will look like this:

    ForEach(data, id: \.self) { item in
        Text(item) // This is the UI of the cell.
            .overlay {
                Button {
                    print("Button pressed")
                    selected = item
                } label: {
                    Color.clear
                }
        }
    }