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:
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?
One way to approach this is as follows:
.onGeometryChange
to save the position of the selected cell in the coordinate space of the grid. This requires naming the coordinate space of the grid.LazyVGrid
.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)
}
}