iosswiftanimationswiftuiswiftui-layout

Cant change zIndex of LazyVGrid element to bring it to the front with animation


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()
        }
    }
}

Solution

  • 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:

    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
                                }
                            }
                    }
                }
            }
        }
    }
    

    Animation