Home > OS >  Swift Struct in Array of Structs not updating to new values
Swift Struct in Array of Structs not updating to new values

Time:10-23

This is my data structure

struct SPPWorkout: Codable {
    static let setKey = "Sets"
    static let exerciseID = "id"
    
    var id: Double? = 0.0
    var duration: String?
    var calories: Int?
    var date: String?
    var exercises: [ExerciseSet]
    
    [...]
}

struct ExerciseSet: Codable {
    let id: String
    let name: String
    var reps: Int
    var weight: Double

    [...]
}

extension ExerciseSet: Equatable {
    static func ==(lhs: ExerciseSet, rhs: ExerciseSet) -> Bool {
        return lhs.id == rhs.id
    }
}

and in a SwiftUI view I'm trying to modify an ExerciseSet from user input

@State private var sppWorkout: SPPWorkout!

                    EditSetPopup(isShowingOverlay: $isShowingOverlay,
                                 update: { reps, weight in
                        guard let editingIndex = editingIndex else { return }
                        sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
                        sppWorkout.exercises[editingIndex].weight = Double(weight) ?? 0.0
                        
                        self.editingIndex = nil
                    })
                }

The issue is here

sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ?? 

and I've tried in all ways to update it, both from the view and with a func in SPPWorkout. I've also tried to replace the object at index

var newSet = ExerciseSet(id: [...], newValues)
self.exercises[editingIndex] = newSet

but in no way it wants to update. I'm sure that somewhere it creates a copy that it edits but I have no idea why and how to set the new values.

Edit: if I try to delete something, it's fine

sppWorkout.exercises.removeAll(where: { $0 == sppWorkout.exercises[index]})

Edit 2: It passes the guard statement and it does not change the values in the array.

enter image description here

Edit 3: At the suggestion below from Jared, I've copied the existing array into a new one, set the new values then tried to assign the new one over to the original one but still, it does not overwrite.

EditSetPopup(isShowingOverlay: $isShowingOverlay,
                                 update: { reps, weight in
                        print(sppWorkout.exercises)
                        guard let editingIndex = editingIndex else { return }
                        
                        var copyOfTheArray = sppWorkout.exercises
                        copyOfTheArray[editingIndex].reps = Int(reps) ?? 0
                        copyOfTheArray[editingIndex].weight = Double(weight) ?? 0.0
                        //Copy of the array is updated correctly, it has the new values
                        
                        sppWorkout.exercises = copyOfTheArray
                        //Original array doesn't get overwritten. It still has old values
                        
                        self.editingIndex = nil

Edit 4: I've managed to make progress by extracting the model into a view model and updating the values there. Now the values get updated in sppWorkout, but even though I call objectWillChange.send(), the UI Update doesn't trigger.

full code:

class WorkoutDetailsViewModel: ObservableObject {
    var workoutID: String!
    @Published var sppWorkout: SPPWorkout!
    
    func setupData(with workoutID: String) {
        sppWorkout = FileIOManager.readWorkout(with: workoutID)
    }
    
    func update(_ index: Int, newReps: Int, newWeight: Double) {
        let oldOne = sppWorkout.exercises[index]
        let update = ExerciseSet(id: oldOne.id, name: oldOne.name, reps: newReps, weight: newWeight)
        sppWorkout.exercises[index] = update
        
        self.objectWillChange.send()
    }
}

struct WorkoutDetailsView: View {
    var workoutID: String!
    @StateObject private var viewModel = WorkoutDetailsViewModel()
    
    var workout: HKWorkout
    var dateFormatter: DateFormatter
    
    @State private var offset = 0
    @State private var isShowingOverlay = false
    
    @State private var editingIndex: Int?
    @EnvironmentObject var settingsManager: SettingsManager
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        if viewModel.sppWorkout != nil {
            VStack {
                ListWorkoutItem(workout: workout, dateFormatter: dateFormatter)
                    .padding([.leading, .trailing], 10.0)
                
                List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
                    let index = viewModel.sppWorkout.exercises.firstIndex(of: exercise) ?? 0
                    
                    DetailListSetItem(exerciseSet: viewModel.sppWorkout.exercises[index], set: index   1)
                        .environmentObject(settingsManager)
                        .swipeActions {
                            Button(role: .destructive, action: {
                                viewModel.sppWorkout.exercises.removeAll(where: { $0 == viewModel.sppWorkout.exercises[index]})
                            } ) {
                                Label("Delete", systemImage: "trash")
                            }
                            
                            Button(role: .none, action: {
                                isShowingOverlay = true
                                editingIndex = index
                            } ) {
                                Label("Edit", systemImage: "pencil")
                            }.tint(.blue)
                        }
                }
                .padding([.leading, .trailing], -30)
                //iOS 16 .scrollContentBackground(.hidden)
            }
            .overlay(alignment: .bottom, content: {
                editOverlay
                    .animation(.easeInOut (duration: 0.5), value: isShowingOverlay)
            })
            .navigationBarBackButtonHidden(true)
            .navigationBarItems(leading: Button(action : {
                do {
                    try FileIOManager.write(viewModel.sppWorkout, toDocumentNamed: "\(viewModel.sppWorkout.id ?? 0).json")
                } catch {
                    Debugger.log(error: error.localizedDescription)
                }
                dismiss()
            }){
                Image(systemName: "arrow.left")
            })
        } else {
            Text("No workout details found")
                .italic()
                .fontWeight(.bold)
                .font(.system(size: 35))
                .onAppear(perform: {
                    viewModel.setupData(with: workoutID)
                })
        }
    }
    
    @ViewBuilder private var editOverlay: some View {
        if isShowingOverlay {
            ZStack {
                Button {
                    isShowingOverlay = false
                } label: {
                    Color.clear
                }
                .edgesIgnoringSafeArea(.all)
                VStack{
                    Spacer()
                    EditSetPopup(isShowingOverlay: $isShowingOverlay,
                                 update: { reps, weight in
                        guard let editingIndex = editingIndex else { return }
                        print(viewModel.sppWorkout.exercises)
                        print("dupa aia:\n")
                        viewModel.update(editingIndex, newReps: Int(reps) ?? 0, newWeight:  Double(weight) ?? 0.0)
                        
                        print(viewModel.sppWorkout.exercises)
                        
                        self.editingIndex = nil
                    })
                        .overlay(
                            RoundedRectangle(cornerRadius: 10)
                                .stroke(Color("popupBackground"),
                                        lineWidth: 3)
                        )
                }
            }
        }
    }
}

CodePudding user response:

So I got a very good explanation on reddit on what causes the problem. Thank you u/neddy-seagoon if you are reading this.

The explanation

. I believe that updating an array will not trigger a state update. The only thing that will, with an array, is if the count changes. So sppWorkout.exercises[index].reps = newReps will not cause a trigger. This is not changing viewModel.sppWorkout.exercises.indices

So all I had to to was modify my List from

List(viewModel.sppWorkout.exercises, id: \.id) 

to

List(viewModel.sppWorkout.exercises, id: \.hashValue)

as this triggers the list update because the hashValue does change when updating the properties of the entries in the list.

CodePudding user response:

For the line List(viewModel.sppWorkout.exercises, id: \.id) { exercise in

Replace with

List(viewModel.sppWorkout.exercises, id: \.self) { exercise in

  • Related