Home > Software design >  State when a property of a custom object changes
State when a property of a custom object changes

Time:03-01

I have an Object like this (simplified):

struct BoxModel: Codable, Identifiable, Hashable {
    var id: Int
    var number: Int
}

I have a two dimensional array of these:

@State private var matrix = generate()

where generate creates a 2D array of the objects: [[BoxModel]]. I then display them in a grid. The grid is pretty complex but here's a very simplified form:

LazyVGrid(columns: columns, spacing: 0 ) {
    ForEach(1...9, id: \.self) { j in
         Text("\(getModel(j).number)")
    }
}

Where getValue gets the item from the array that corresponds to the grid location. Since the grid is complex, I need to do some arithmetic in a separate function to do this.

Below this is a button. When the user taps it, it changes the number property of one of the objects in the array. However, the number doesn't update. I thought it would since the array is a @State property.

Is there a way to make it update?

Here's an extremely simplified form of the ContentView:

struct ContentView: View {

@State private var matrix = generate()


var body: some View {
    NavigationView {
        GeometryReader { geometry in
            ScrollView {
                VStack {

                    let columns = [GridItem(.fixed(50), spacing: 0),
                                     GridItem(.fixed(50), spacing: 0),
                                     GridItem(.fixed(50), spacing: 0)]
                    

                    LazyVGrid(columns: columns, spacing: 0 ) {
                        ForEach(1...9, id: \.self) { j in
                            Text("\(getModel(j).number)")
                        }
                    }
                    .padding()
                    Button {
                        var model = getModel(0)
                        model.number = 5
                    } label: {
                        Text("Click Me")
                    }
                }
            }
        }
    }
}


func getModel(_ i: Int) -> BoxModel {
    // very simplified
    return matrix[0][i]
}

CodePudding user response:

Change the code in your Button action closure.

var model = getModel(0)
model.number = 5

to

if let index = matrix[0].firstIndex(of: getModel(1)) {
    matrix[0][index].number = 5
}

I think you're applying the concept of class to a struct, check out the Difference between class and struct

CodePudding user response:

The reason you are not seeing the update is because; BoxModel is a struct and there is no real change to matrix, only the content of one of its item has changed.

There are a number of ways to answer your question: "Is there a way to make it update?". One simple minded way is shown in the code below, where BoxModel is a class, and there is @State private var refresh that is changed to trigger a view refresh.

class BoxModel: Codable, Identifiable, Hashable {
    var id: Int
    var number: Int

    init(id: Int, number: Int) {
        self.id = id
        self.number = number
    }

    static func == (lhs: BoxModel, rhs: BoxModel) -> Bool {
        lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

struct ContentView: View {
    @State private var refresh = false // <-- here
    @State private var matrix = generate()
    
    var body: some View {
        NavigationView {
            GeometryReader { geometry in
                ScrollView {
                    VStack {
                        
                        let columns = [GridItem(.fixed(50), spacing: 0),
                                       GridItem(.fixed(50), spacing: 0),
                                       GridItem(.fixed(50), spacing: 0)]
                        
                        
                        LazyVGrid(columns: columns, spacing: 0 ) {
                            ForEach(1...9, id: \.self) { j in
                                Text("\(getModel(j).number)")
                            }
                        }.padding(refresh ? 0 : 0) // <-- here
                        .padding()
                        Button {
                            var model = getModel(0)
                            model.number = 5
                            refresh.toggle() // <-- here
                        } label: {
                            Text("Click Me")
                        }
                    }
                }
            }
        }
    }
}

Although this should work, I suggest you re-structure your code and use a ObservableObject for your BoxModel. Something like this example code:

class BoxModel: ObservableObject {
    @Published var matrix: [[Box]] = generate()

    func getBox(_ j: Int) -> Box {
        // ...
    }

    func updateBox(_ j: Int, with n: Int) {
        // ...
    }
}

struct Box: Codable, Identifiable, Hashable {
    var id: Int
    var number: Int
}

struct ContentView: View {
    @StateObject var boxModel = BoxModel()
    
    var body: some View {
        NavigationView {
            GeometryReader { geometry in
                ScrollView {
                    VStack {
                        let columns = [GridItem(.fixed(50), spacing: 0),
                                       GridItem(.fixed(50), spacing: 0),
                                       GridItem(.fixed(50), spacing: 0)]
                        
                        LazyVGrid(columns: columns, spacing: 0 ) {
                            ForEach(1...9, id: \.self) { j in
                                Text("\(boxModel.getBox(j).number)")
                            }
                        }
                        .padding()
                        Button {
                            boxModel.updateBox(0, with: 5)
                        } label: {
                            Text("Click Me")
                        }
                    }
                }
            }
        }
    }
    
}

Note, depending on how you implement getModel()/getBox() you may need to adjust ForEach(1...9, id: \.self) {...} to ForEach(0...8, id: \.self) {...} since array index start at zero.

  • Related