0

I'm developing an App (using Xcode 11.3.1, target device: iPad) for our company's engineers to report on work they do. Part of the app needs to be an editable list of parts they've used.

I've replicated the mechanisms I'm trying to implement (Observed Object/@Binding etc) in a simple 'Person List' test project (full project code below).

I'm still trying to learn SWiftUI so I've probably done something stupid in my code.

The objective here is to create a dynamic list with editable fields. When the code is previewed it seems to work perfectly, however, things start to go wrong after elements are deleted. (Deleting the last element causes "Fatal error: Index out of range".

If you add new elements after deleting some, the new elements have blank textFields and are un-editable.

I would very much appreciate any help anyone can offer.

import SwiftUI

struct EditView: View {
    @Binding var person:Person
    var body: some View {
        HStack{
            Group{
                TextField("name1", text: $person.name1)
                TextField("name2", text: $person.name2)
            }.frame(width:150)
            .font(.headline)
                .padding(.all, 3)
                .overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.blue, lineWidth: 1))
        }
    }
}
struct Person:Identifiable, Equatable{
    var id:UUID
    var name1:String
    var name2:String
}
class PersonList: ObservableObject {
    @Published var individuals = [Person]()// Array of Person structs
}
struct ContentView: View {
    @ObservedObject var people = PersonList()// people.individuals = [Person] array
    @State private var edName1:String = "" //temporary storage for adding new member
    @State private var edName2:String = "" //temporary storage for adding new member
    var elementCount:Int{
        let c = people.individuals.count
        return c
    }
    // arrays for testing - adds random names from these (if input field '1st name' is empty)...
    var firstNames = ["Nick","Hermes","John","Hattie","Nicola","Alan", "Dwight", "Richard"]
    var surnames = ["Fury","Smith","Jones","Hargreaves","Bennylinch", "Davidson","Lucas","Partridge"]
    var body: some View {
        NavigationView{
            VStack{
                HStack{
                    Text("Add person:")
                        .padding(.all, 5)
                        .frame(alignment: .leading)
                    TextField("1st name", text: $edName1)
                        .frame(width:150)
                        .padding(.all, 5)
                        .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.blue, lineWidth: 2))
                    TextField("2nd name", text: $edName2)
                        .frame(width:150)
                        .padding(.all, 5)
                        .overlay(RoundedRectangle(cornerRadius: 8)
                            .stroke(Color.blue, lineWidth: 2))
                    //  Button...
                    Image(systemName: "plus.square")
                        .font(.title)
                        .foregroundColor(.orange)
                        .onTapGesture {
                            if self.edName1 == ""{
                                self.edName1 = self.firstNames.randomElement() ?? "⁉️"
                                self.edName2 = self.surnames.randomElement() ?? "⁉️"
                            }
                            self.people.individuals.append(Person(id: UUID(), name1: self.edName1, name2: self.edName2))
                            self.edName1 = ""
                            self.edName2 = ""
                            print("Element count: \(self.elementCount)")
                    }
                    Spacer()
                    //  Button...sort
                    Image(systemName: "arrow.up.arrow.down.square")
                        .font(.title)
                        .padding(.all,4)
                        .foregroundColor(.blue)
                        .onTapGesture {
                            self.people.individuals.sort{ // sort list alphabetically by name2
                                $0.name2 < $1.name2
                            }
                    }
                    //  Button...reverse order
                    Image(systemName: "arrow.uturn.up.square")
                        .font(.title)
                        .padding(.all,8)
                        .foregroundColor(.blue)
                        .onTapGesture {
                            self.people.individuals.reverse()
                    }
                }.padding(.all,8)
                    .overlay(RoundedRectangle(cornerRadius: 12)
                        .stroke(Color.orange, lineWidth: 2))
                List{
                    ForEach(people.individuals){individual in
                        HStack{
                          EditView(person: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!])

                            Text("\(individual.name1) \(individual.name2)")
                                .frame(width: 200, alignment: .leading)
                                .padding(.all, 3)
                            Text("\(self.people.individuals.firstIndex(of: individual)!)")
                            Spacer()

                            Image(systemName: "xmark.circle.fill").font(.title).foregroundColor(.red)//❌
                                .onTapGesture {
                                    let deletedElement = self.people.individuals.remove(at: self.people.individuals.firstIndex(of: individual)!)
                                    print("Deleted element:\(deletedElement) Element count: \(self.elementCount)")
                            }
                        }
                    }//.onDelete(perform: deleteRow)// an alternative to the red xmark circle
                }
            }.navigationBarTitle("People List (\(elementCount))")
        }.navigationViewStyle(StackNavigationViewStyle())
    }
    func deleteRow(at offsets: IndexSet){
        self.people.individuals.remove(atOffsets: offsets)
    }

}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
          .environment(\.colorScheme, .dark)
    }
}
  • hint: your problem is here: EditView(member: self.$people.members[self.people.members.firstIndex(of: member)!]) – Chris Jan 15 '20 at 12:39
  • @Chris - thanks for the hint. I'll see if I can work out what I've done wrong. – philslinkee Jan 15 '20 at 13:46
  • After refactoring the code a little, the line @Chris refers to above is now ; ```EditView(person: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!])``` . I'm still struggling to work out what's wrong with my code. Admittedly my knowledge of SwiftUI is sadly lacking at present. – philslinkee Jan 20 '20 at 16:44
  • I have tried various alterations to this line of code but none will even compile – philslinkee Jan 20 '20 at 17:07
  • My initial inclination was to write this line as: EditView(person: individual), however this causes compile errors higher up in the code. I think because it doesn't pass the binding through to the child view. It seems to require - EditView(person: Binding) - this is what I've attempted with the example above. It was the only way I could get the "$" binding in there. – philslinkee Jan 21 '20 at 12:12
  • I give up! I think SwiftUI is so much better than previous versions but I get the impression it's not as developed as it should be... More likely that I'm just not smart enough. – philslinkee Jan 21 '20 at 15:36

1 Answers1

0

Thanks to KRJW and Natalia Panferova for help with different aspects of this code. There are no 'index out of range' errors now and also it is possible to delete rows without causing problems with adding items. I'm sharing this answer because I believe it's a very useful mechanism to create editable lists.

import SwiftUI

class Person: ObservableObject, Identifiable, Equatable {
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.id == rhs.id
    }

    @Published var id:UUID
    @Published var name1:String
    @Published var name2:String

    init(id: UUID, name1: String, name2: String, isEditable: Bool){
        self.id = id
        self.name1 = name1
        self.name2 = name2
    }
}


struct EditView: View {
    @ObservedObject var person: Person
    var body: some View {
        VStack{
            HStack{
                Group{
                    TextField("name1", text: $person.name1)
                    TextField("name2", text: $person.name2)
                }//.frame(width:200)
                    .font(.headline)
                    .padding(.all, 3)
                    .overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.blue, lineWidth: 1))
            }.id(person.id)
        }
    }
}




struct ContentView: View {
    @State var people = [Person]()//try! ObservableArray<Person>(array: []).observeChildrenChanges(Person.self)// people.individuals = [Person] array

    @State private var edName1:String = "" //temporary storage for adding new member
    @State private var edName2:String = "" //temporary storage for adding new member

    // arrays for testing - adds random names from these (if input field '1st name' is empty)...
    var firstNames = ["Nick","Hermes","John","Hattie","Nicola","Alan", "Dwight", "Richard","Turanga", "Don","Joey"]
    var surnames = ["Farnsworth","Fry","Wong","Zoidberg","Conrad","McDougal","Power","Clampazzo","Brannigan","Kroker","Leela"]

    var body: some View {
        NavigationView{
            VStack{
                HStack{
                    Text("Add person:")
                        .padding(.all, 5)
                        .frame(alignment: .leading)
                    TextField("1st name", text: $edName1)
                        //.frame(width:150)
                        .padding(.all, 5)
                        .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.blue, lineWidth: 2))
                    TextField("2nd name", text: $edName2)
                        //.frame(width:150)
                        .padding(.all, 5)
                        .overlay(RoundedRectangle(cornerRadius: 8)
                            .stroke(Color.blue, lineWidth: 2))
                    //  Button...
                    Image(systemName: "plus.circle")
                        .font(.largeTitle)
                        .foregroundColor(.orange)
                        .onTapGesture {
                            if self.edName1 == ""{
                                self.edName1 = self.firstNames.randomElement() ?? "⁉️"
                                self.edName2 = self.surnames.randomElement() ?? "⁉️"
                            }
                            self.people.append(Person(id: UUID(), name1: self.edName1, name2: self.edName2, isEditable: false))
                            self.edName1 = ""
                            self.edName2 = ""
                            print("Element count: \(self.people.count)")
                    }
                    Spacer()
                    //  Button...sort
                    Image(systemName: "arrow.up.arrow.down.square")
                        .font(.title)
                        .padding(.all,4)
                        .foregroundColor(.blue)
                        .onTapGesture {
                            self.people.sort{ // sort list alphabetically by name2
                                $0.name2 < $1.name2
                            }
                    }
                    //  Button...reverse order
                    Image(systemName: "arrow.uturn.up.square")
                        .font(.title)
                        .padding(.all,8)
                        .foregroundColor(.blue)
                        .onTapGesture {
                            self.people.reverse()
                    }
                }.padding(.all,8)
                    .overlay(RoundedRectangle(cornerRadius: 12)
                        .stroke(Color.orange, lineWidth: 2))



                List {
                    ForEach(self.people) { person in
                        EditView(person: person)
                    }.onDelete(perform: deleteRow)
                }



            }.navigationBarTitle("People List (\(self.people.count))")
        }.navigationViewStyle(StackNavigationViewStyle())
    }

    func deleteRow(at offsets: IndexSet){
        self.people.remove(atOffsets: offsets)
        print(self.people.count)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
           // .environment(\.colorScheme, .dark)
    }
}

The code is all bundled together for convenience. Feel free to copy and paste and play around.