swiftswiftuiswiftui-listswiftui-foreach

SwiftUI's List displays the same row twice in ForEach


can anyone please help me to solve this? Please paste the same code in your Xcode and run it. Please follow the below steps:

  1. Select Date as 1 June 2022, Time 1 as 07:00 and Time2 as 15:00. Enter load1 as 7 and load2 as 8. Click on Add load. It will show up the date, time1 and time2 with total amount.
  2. Similarly keeping the time same(07:00-15:00) and changing the dates to 3rd of June and 4th of June and with different values of load1 and load2, click on Add load.
  3. Now click on the Edit button and delete the data of 4th June and 1st June and then Enter a new data using 2nd June. The date and total amount of 3rd June automatically changes to 2nd of June. So I now have both data as 2nd June. Also the Total comes up with a different value. Why is this happening? any idea where am I doing wrong here?
struct Task : Identifiable {
    var id = String()
    var toDoItem = String()
    var amount : Float = 0 //<-- Here
}

class TaskStore : ObservableObject {
    @Published var tasks = [Task]()
}

struct Calculation: View {
    
    @State var load1 = Float()
    @State var load2 = Float()
    @State var gp : Float = 0
    @State var rate: Float = 0
    @ObservedObject var taskStore = TaskStore()
    @State private var birthDate = Date()
    @State private var time1 = Date()
    @State private var time2 = Date()
    func addNewToDo() {
        let formatter = DateFormatter()
        formatter.dateFormat = "dd/MM/yyyy"
        let dayoftime = formatter.string(from: birthDate)
        let formatter2 = DateFormatter()
        formatter2.dateFormat = "HH:mm"
        let timeof1 = formatter2.string(from: time1)
        let timeof2 = formatter2.string(from: time2)
        
        
        taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), toDoItem:  "\(dayoftime) \(timeof1)-\(timeof2) total = \(rate.description) ", amount: rate))
        
        //gp += rate
    }
    
    
    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    VStack(spacing: 1) {
                        Section(header: Text("Date") .foregroundColor(.black)
                        ){
                            DatePicker(selection: $birthDate, displayedComponents: [.date]){Text(" Time")
                            }
                        }
                        
                        VStack(spacing: 1) {
                            Section(header: Text("Time1") .foregroundColor(.black)){
                                DatePicker(selection: $time1, displayedComponents: [.hourAndMinute]){Text(" Time 1")
                                }
                            }
                            VStack(spacing: 1) {
                                Section(header: Text("Time2") .foregroundColor(.black)
                                ){ DatePicker(selection: $time2, displayedComponents: [.hourAndMinute]){Text(" Time 1")
                                }
                                }
                            }
                            List {
                                
                                Section(header:Text("load 2"))
                                {
                                    TextField("Enter value of load 1", value: $load1, format: .number)
                                    TextField("Enter value of load 1", value: $load2, format: .number)
                                }
                                
                                HStack {
                                    Button(String(format: "Add Load"), action: {
                                        
                                        print(Rocky(mypay: rate))
                                        gp += rate
                                    })
                                    
                                    Button(action: {
                                        addNewToDo()
                                        Rocky(mypay: rate)
                                    },
                                           label: {
                                        Text(" ")
                                    })
                                }
                                
                                ForEach(self.taskStore.tasks) { task in
                                    Text(task.toDoItem)
                                }
                                .onMove(perform : self.move)
                                .onDelete(perform : self.delete) //For each
                                
                            }
                            .navigationBarTitle("SHIFTS")
                            .navigationBarItems(trailing: EditButton()) //List
                            
                            Text("Total = $\(gp) ")
                            
                        }.onAppear()
                        
                    }
                }
            }
        }
    }
    func Rocky(mypay: Float)
    {
        rate = load1 + load2
        print("Sus \(gp)")
    }
    func move(from source : IndexSet, to destination : Int)
    {
        taskStore.tasks.move(fromOffsets: source, toOffset: destination)
    }
    func delete(at offsets : IndexSet) {
        if let index = offsets.first {  //<-- Here
            let task = taskStore.tasks[index]
            gp -= task.amount
        }
        taskStore.tasks.remove(atOffsets: offsets)
    }
}

Solution

  • The issue is that ForEach needs your items to be uniquely identifiable.

    You're using an autogenerated id for your tasks that is basically calculated as a current count of tasks in the storage:

    taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), ......`
    

    So here's what's happening:

    1. You add first three tasks, they get the IDs of "1", "2" and "3".
    2. You remove the tasks with the IDs "1" and "3". The only task that 's remaining in the array is the one with the ID of "2".
    3. You're adding another task to the list. When creating a task, the count of the tasks in the array is 1, so the new task gets the ID of "2". So now you have two different tasks in the array with the same IDs.
    4. When SwiftUI renders you view, the ForEach method distinguishes your tasks by ID. It sees that it should render two rows of the list and both of them have the ID of "2". So it finds the first item with the ID equal to "2" and renders it twice because it assumes that all items in the array have unique IDs (which the should have really).

    To solve this, use a UUID as an identifier for a task. You can also create an initializer that doesn't take an ID as an argument because the task will create a unique identifier for itself:

    struct Task: Identifiable {
        var id: UUID
        var toDoItem: String
        var amount: Float
    
        init(toDoItem: String, amount: Float) {
            self.id = UUID()
            self.toDoItem = toDoItem
            self.amount = amount
        }
    }
    

    When creating a new task, use this new initializer without assigning items an explicit identifier:

    taskStore.tasks.append(
        Task(
            toDoItem:  "\(dayoftime) \(timeof1)-\(timeof2) total = \(rate.description) ",
            amount: rate
        )
    )
    

    That's it, now all tasks will have unique identifiers and will render correctly.