iosswiftuilazyvgrid

How to display a View for selected item in LazyVGrid under a row where that grid is presented?


This is how I define my grid:

LazyVGrid(columns: [GridItem(.adaptive(minimum: width), spacing: 10)], spacing: 10) {
    ForEach(story.sorted, id: \.self) { book in
        BookElementView(book: book, width: width)
            .onTapGesture {
                // activate selection
            }
    }
}

This is how it looks like in the app:

enter image description here

When I select any item in that grid, I would like to extend space just below row where that item is placed and display there any view related to this item.

Is it possible? Does SwiftUI give us any possibility to manage rows in grid, or just items in grid?


Solution

  • One way to approach this is as follows:

    Adding bottom padding to the cell will cause .onGeometryChange to kick in after the selection is made. So this allows the y-position of the selected cell to be measured in response to the selection. If the selection is changed to another cell, it is best to collapse the previous selection before applying the new selection. This way, the y-position of the new selection will always be recorded correctly.

    The reason for applying the overlay to the grid, instead of to the ScrollView, is so that tap gestures can be attached to the overlay without blocking the scroll gesture for the scroll view.

    Here is an elaborated example to show it all working:

    struct ContentView: View {
        let story: [String] = [
            "Ge", "Ex", "Le", "Nu", "De", "Jos",
            "Jg", "Ru", "1Sa", "2Sa", "1Ki", "2Ki",
            "1Ch", "2Ch", "Ezr", "Ne", "Es", "Job",
            "Ps", "Pr", "Ec", "Ca", "Isa", "Jer",
            "La", "Eze", "Da", "Ho", "Joe", "Am",
            "Ob", "Jon", "Mic", "Na", "Hab", "Zep",
            "Hag", "Zec", "Mal",
            "Mt", "Mr", "Lu", "Joh", "Ac", "Ro"
        ]
        let width: CGFloat = 50
        let expandedHeight: CGFloat = 100
        let coordinateSpace = "Grid"
        @State private var selectedBook: String?
        @State private var ySelected = CGFloat.zero
    
        var body: some View {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: width), spacing: 10, alignment: .top)], spacing: 10) {
                    // ForEach(story.sorted, id: \.self) { book in
                    ForEach(story, id: \.self) { book in
                        let isSelected = selectedBook == book
                        BookElementView(book: book, width: width)
                            .onTapGesture {
                                if selectedBook == nil {
                                    selectedBook = book
                                } else {
                                    let newSelection = isSelected ? nil : book
                                    withAnimation {
                                        selectedBook = nil
                                    } completion: {
                                        selectedBook = newSelection
                                    }
                                }
                            }
                            .padding(.bottom, isSelected ? expandedHeight : 0)
                            .onGeometryChange(for: CGFloat.self) { geo in
                                geo.frame(in: .named(coordinateSpace)).maxY
                            } action: { maxY in
                                if isSelected {
                                    ySelected = maxY
                                }
                            }
                    }
                }
                .padding()
                .frame(maxHeight: .infinity, alignment: .top)
                .coordinateSpace(name: coordinateSpace)
                .overlay(alignment: .top) {
                    if let selectedBook {
                        Text("Expanded detail for \(selectedBook) here")
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .background { Color.yellow }
                            .padding(.top, 10)
                            .frame(height: expandedHeight)
                            .padding(.top, ySelected - expandedHeight)
                            .onTapGesture { self.selectedBook = nil }
                    }
                }
            }
            .animation(.easeInOut, value: selectedBook)
        }
    }
    
    struct BookElementView: View {
        let book: String
        let width: CGFloat
    
        var body: some View {
            Text(book)
                .frame(width: width, height: width)
                .background(.gray)
        }
    }
    

    Animation