Search code examples
arraysobservableswiftuiobservedobject

SwiftUI Strange/unexpected behaviour when deleting elements in array of observedObjects


I am trying to create an editable 'parts list' in an app I'm trying to develop. It's been a rough road getting this far - many unexpected hurdles and obstacles, most of which have now been overcome (thanks to 'krjw').
So, I'm now at a stage where items can be added to a dynamic list with editable fields. The problem arises after an item is deleted...subsequent additions to the list (equal to the number of deletions) cannot be edited. I've bundled the relevant code together for convenience below:

import SwiftUI

//  Thanks to KRJW on StackOverflow for help getting this far...

struct PartEditView: View {
    @ObservedObject var partToEdit:Part
    var body: some View {
        VStack{
            HStack(alignment: .bottom){
                TextField("Description", text: $partToEdit.description)
                    .frame(width: 350)
                    .padding(.all,5)
                    .cornerRadius(2)
                    .overlay(
                        RoundedRectangle(cornerRadius: 4)
                            .stroke(Color.red, lineWidth: 1))

                Spacer()

                TextField("Price", text: $partToEdit.price).keyboardType(.decimalPad)
                    .frame(width: 80)
                    .padding(.all,5)
                    .cornerRadius(2)
                    .overlay(
                        RoundedRectangle(cornerRadius: 4)
                            .stroke(Color.red, lineWidth: 1))

                Spacer()

                TextField("Quantity", text: $partToEdit.qty).keyboardType(.decimalPad)
                    .frame(width: 60)
                    .padding(.all,5)
                    .cornerRadius(2)
                    .overlay(
                        RoundedRectangle(cornerRadius: 4)
                            .stroke(Color.red, lineWidth: 1))

                Spacer()
                VStack(alignment: .leading){
                    //Text("Line total").font(.caption).padding(.bottom,10)
                    Text(String(format: "%.2f", Double(partToEdit.linePrice)))
                        .frame(width:90,alignment: .leading)
                        .padding(.all,5)
                        .cornerRadius(4)
                        .overlay(
                            RoundedRectangle(cornerRadius: 4)
                                .stroke(Color.blue, lineWidth: 1))
                }
            }
        }
    }
}


class Part: ObservableObject, Identifiable, Equatable {
    static func == (lhs: Part, rhs: Part) -> Bool {
        return lhs.id == rhs.id
    }
    //part line item (desc, price, qty, line price)
    @Published var id: UUID
    @Published var description:String
    @Published var price:String
    @Published var qty:String
    var linePrice:Double{
        let itemPrice = Double(price) ?? 0
        let quantity = Double(qty) ?? 0
        return itemPrice * quantity
    }
    init(id:UUID, description:String, price:String, qty:String) {
        self.id = id
        self.description = description
        self.price = price
        self.qty = qty
    }
}


struct ContentView: View {
    @State var parts = [Part]()

    var body: some View {
        VStack{
            HStack{
                Text("Add line ")

                Image(systemName: "plus.circle").font(.largeTitle)
                    .onTapGesture {
                        let newPart:Part = Part(id: UUID(), description: "any.....thing", price: "", qty: "1")
                        self.parts.append(newPart)
                }
            }

            List{
                ForEach(parts){part in
                    PartEditView(partToEdit: part)
                }.onDelete(perform: deleteRow)
            }
            HStack{
                Spacer()
                Text("Total: ")
                Text(String(self.parts.reduce(0){$0 + $1.linePrice}))
            }
            Spacer()
        }
    }
    func deleteRow(at offsets: IndexSet){
        self.parts.remove(atOffsets: offsets)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I suspect that array indexing is somehow getting mixed up.
If anyone can shed any light on this I'd very much appreciate it.


Solution

  • Looks like the row doesn't get updated. I've solved it by adding an id to your PartEditView HStack that has all the TextFields, like so:

    struct PartEditView: View {
        @ObservedObject var partToEdit:Part
        var body: some View {
            VStack{
                HStack(alignment: .bottom){
                    TextField("Description", text: $partToEdit.description)
                        .frame(width: 350)
                        .padding(.all,5)
                        .cornerRadius(2)
                        .overlay(
                            RoundedRectangle(cornerRadius: 4)
                                .stroke(Color.red, lineWidth: 1))
    
                    Spacer()
    
                    TextField("Price", text: $partToEdit.price).keyboardType(.decimalPad)
                        .frame(width: 80)
                        .padding(.all,5)
                        .cornerRadius(2)
                        .overlay(
                            RoundedRectangle(cornerRadius: 4)
                                .stroke(Color.red, lineWidth: 1))
    
                    Spacer()
    
                    TextField("Quantity", text: $partToEdit.qty).keyboardType(.decimalPad)
                        .frame(width: 60)
                        .padding(.all,5)
                        .cornerRadius(2)
                        .overlay(
                            RoundedRectangle(cornerRadius: 4)
                                .stroke(Color.red, lineWidth: 1))
    
                    Spacer()
                    VStack(alignment: .leading){
                        //Text("Line total").font(.caption).padding(.bottom,10)
                        Text(String(format: "%.2f", Double(partToEdit.linePrice)))
                            .frame(width:90,alignment: .leading)
                            .padding(.all,5)
                            .cornerRadius(4)
                            .overlay(
                                RoundedRectangle(cornerRadius: 4)
                                    .stroke(Color.blue, lineWidth: 1))
                    }
                }
                .id(partToEdit.id)
            }
        }
    }
    

    This way the row will be updated for each Part. The previously deleted row won't be re-used by SwiftUI, instead a new one will be created.