I have the following example view with no zIndex modifications. Currently, when selecting a grid item, it will animate to the center of the screen. However, this does not change zIndex and items will stay below others.
I tried adding ZStacks to various parts of the code, with a .zIndex() on the Rectangle, none of which worked.
The closest i got to a fix: adding .id() of the selected item to the LazyVGrid, with a .zindex conditionally setting it on the GeometryReader. While this sets hierarchy properly, each time an item is selected, the entire grid flashes and there is no animating the item from its grid position to the center.
struct SwiftUIView: View {
let colors: [Color] = [.red, .blue, .green, .yellow, .purple, .orange]
let columns = [GridItem(.flexible()), GridItem(.flexible())]
@State private var selectedItem: Int? = nil
@State private var selectedItemPosition: CGRect? = nil
var body: some View {
GeometryReader { screenGeometry in
LazyVGrid(columns: columns, spacing: 20) {
ForEach(colors.indices, id: \.self) { index in
GeometryReader { geometry in
let isSelected = selectedItem == index
Rectangle()
.fill(colors[index])
.cornerRadius(12)
.frame(width: 150, height: 200)
.shadow(radius: isSelected ? 10 : 0)
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
if isSelected {
selectedItem = nil
selectedItemPosition = nil
} else {
selectedItem = index
selectedItemPosition = geometry.frame(in: .global)
}
}
}
.offset(
x: isSelected ? (screenGeometry.size.width / 2 - geometry.frame(in: .global).midX) : 0,
y: isSelected ? (screenGeometry.size.height / 2 - geometry.frame(in: .global).midY) : 0
)
}
.frame(height: 200) // Needed to ensure GeometryReader does not shrink
}
}
.padding()
}
}
}
Changing the zIndex
of items in a LazyVGrid
does not work well, as demonstrated in this post. Changing the .id
of the items is a way to work around the issue. However, this may make it harder to animate the changes, as you have found.
An alternative way to solve is to use .matchedGeometryEffect
to position the visible items:
GeometryReader
to determine the screen size, because it will be centered automatically.zIndex
that is higher than the other items too (but lower than the currently selected item). If this is not done, an item that is returning to its grid position will travel under the tiles that follow it in the layout.struct SwiftUIView: View {
let colors: [Color] = [.red, .blue, .green, .yellow, .purple, .orange]
let columns = [GridItem(.flexible()), GridItem(.flexible())]
let centeredItemId = -1
@State private var selectedItem: Int? = nil
@State private var lastSelectedItem: Int? = nil
@Namespace private var ns
var body: some View {
LazyVGrid(columns: columns, spacing: 20) {
// Placeholders for tiles
ForEach(0..<colors.count, id: \.self) { index in
Color.clear
.frame(width: 150, height: 200)
.matchedGeometryEffect(id: index, in: ns)
}
}
.padding()
.background {
// Placeholder for the centered item
Color.clear
.frame(width: 150, height: 200)
.matchedGeometryEffect(id: centeredItemId, in: ns)
}
.overlay {
ZStack {
// The visible items
ForEach(Array(colors.enumerated()), id: \.offset) { index, color in
let isSelected = selectedItem == index
RoundedRectangle(cornerRadius: 12)
.fill(color)
.shadow(radius: isSelected ? 10 : 0)
.zIndex(isSelected ? 2 : (lastSelectedItem == index ? 1 : 0))
.matchedGeometryEffect(id: isSelected ? centeredItemId : index, in: ns, isSource: false)
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
if isSelected {
selectedItem = nil
} else {
selectedItem = index
}
} completion: {
lastSelectedItem = selectedItem
}
}
}
}
}
}
}