iosswiftswiftuilazyvgrid

LazyVGrid is not redrawn on .id() change


I'm trying to update LazyVGrid item properties when tapped and it worked on plain Ints but stopped working when numbers are wrapped in collection.

code that doesn't work:


import SwiftUI

struct Item : Identifiable, Hashable {
    var id: Int
}

let data = (1...12).map { index in
    Item(id: index)
}

struct ZIndexInLazyVGridUpdateContainerExperiment: View {
    @State private var selectedItemIndex: Int = 5
    
    var body: some View {

        ZStack {
            Color.black.ignoresSafeArea()
            ScrollView {
                LazyVGrid(columns: [GridItem(), GridItem(), GridItem()]) {
                    ForEach(data) { item in
                        Rectangle()
                            .fill(Color(
                                red: 0,
                                green: 0.5,
                                blue: selectedItemIndex == item.id ? 1 : Double(item.id)/24.0 + 0.5))
                            .zIndex(selectedItemIndex == item.id  ? 1 : 0)
                            .frame(width: 180, height: 180)
                            .border(.black)
                            .rotationEffect(.degrees(45))
                            .onTapGesture {
                                selectedItemIndex = item.id
                                print("selected item \(selectedItemIndex)")
                            }
                    }
                    .id(selectedItemIndex)
                }
            }
        }
    }
}

code that works, that was slightly changed:

import SwiftUI

struct ZIndexInLazyVGridUpdateExperiment: View {
    @State private var selectedItemIndex: Int = 5

    var body: some View {

        ZStack {
            Color.black.ignoresSafeArea()
            ScrollView {
                LazyVGrid(columns: [GridItem(), GridItem(), GridItem()]) {
                    ForEach(1..<13) { index in
                        Rectangle()
                            .fill(Color(
                                red: selectedItemIndex == index ? 1 : Double(index)/24.0 + 0.5,
                                green: 0,
                                blue: 0))
                            .zIndex(selectedItemIndex == index ? 1 : 0)
                            .frame(width: 180, height: 180)
                            .border(.black)
                            .rotationEffect(.degrees(45))
                            .onTapGesture {
                                selectedItemIndex = index
                                print("selected item \(selectedItemIndex)")
                            }
                    }
                    .id(selectedItemIndex)
                }
            }
        }
    }
}

Previously (in working code version), item, when tapped, would print to console, update rectangle color and update zIndex. After wrapping Int in Item struct, tapping still is logged, but nothing in LazyVGrid changes visually. I can't figure out what went wrong.

enter image description here


Solution

  • As Workingdog showed, you need to have the selected id on the LazyVGrid, not on the ForEach. I suspect that the lazy optimisation part of the LazyVGrid works out that none of the bound IDs are changing in the ForEach, so it doesn't redraw the view.

    If you remove the LazyVGrid so that you only have the rectangles in the scroll view, changing the id of the ForEach is sufficient.

    In addition to moving the id for the selected item to the LazyVGrid I would avoid using the .id property; this doesn't change the behaviour of your code, but it is cleaner. Identifiable means you can compare directly

    import SwiftUI
    
    struct Item : Identifiable, Hashable {
        var id: Int
    }
    
    let data = (1...12).map { index in
        Item(id: index)
    }
    
    struct ContentView: View {
        @State private var selectedItem = data[5]
        
        var body: some View {
            
            ZStack {
                Color.black.ignoresSafeArea()
                ScrollView {
                    LazyVGrid(columns: [GridItem(), GridItem(), GridItem()]) {
                        ForEach(data) { item in
                            Rectangle()
                                .fill(Color(
                                    red: 0,
                                    green: 0.5,
                                    blue: selectedItem == item ? 1 : Double(item.id)/24.0 + 0.5))
                                .zIndex(item == selectedItem  ? 1 : 0)
                                .frame(width: 180, height: 180)
                                .border(.black)
                                .rotationEffect(.degrees(45))
                                .onTapGesture {
                                    selectedItem = item
                                    print("selected item \(selectedItem.id)")
                                }
                        }
                       
                    }
                     .id(selectedItem)
                }
            }
        }
    }