1

This is a very similar problem to one I had before (which no one could answer). I'm trying to create a dynamic list in which I can edit elements. As far as I can gather, the recommended way to do this is to have an EditView, with bindings, that's activated by a NavigationLink in the LIst.
So, I've done that. It appears to work at first, until I realised that each NavigationLink would only work once (is this a bug?). I can't think what I could have done wrong to cause that.
Then I thought perhaps I can switch to in-place editing by having the EditView in the List. I devised a theoretical way to do this, then tried it in my code. And at first it seemed to work great. However, if 'edit in place' is on, deleting the last element causes 'Fatal error: Index out of range'.
I've bundled my whole code into one file so you can just copy and paste into Xcode to try for yourself.
I'm starting to think that maybe XCode 11.3.1 is far from the finished article, yet.

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:200)
            .font(.headline)
                .padding(.all, 3)
                .overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.blue, lineWidth: 1))
        }.navigationBarTitle("Edit entry")
    }
}
struct Person:Identifiable, Equatable{
    var id:UUID
    var name1:String
    var name2:String
    var isEditable:Bool
}
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
    @State private var allowEditing:Bool = false
    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","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.individuals.append(Person(id: UUID(), name1: self.edName1, name2: self.edName2, isEditable: false))
                            self.edName1 = ""
                            self.edName2 = ""
                            print("Element count: \(self.elementCount)")
                    }
                    Toggle(isOn: $allowEditing){Text("edit in place")}.padding(.all,5).overlay(RoundedRectangle(cornerRadius: 8)
                        .stroke(Color.red, lineWidth: 2))
                    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{
                            if self.allowEditing{
                                //Toggle to edit in place
                                Toggle(isOn: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!].isEditable){
                                    Text("edit").font(.headline).foregroundColor(.green).opacity(individual.isEditable ? 1.0 : 0.4)
                                }.frame(width:100)
                            }

                            if individual.isEditable{
                                EditView(person: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!])
                            }
                            else{
                                NavigationLink(destination:EditView(person: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!])){
                                    Text("\(individual.name1) \(individual.name2)")
                                        .frame(width: 200, alignment: .leading)
                                        .padding(.all, 3)
                                }// link
                            }
                        }
                    }.onDelete(perform: deleteRow)
                }
            }.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)
    }
}

Can anyone shed any light on this? I can't find anything to help me.
UPDATE: Thanks to 'krjw' for pointing out the single use NavLink problem does not happen on a real device.
The 'last element delete' issue seems to be something to do with an active binding being present in the element's view.

16
  • The single use bug does not happen on device. Just tested it in Simulator and on my device and on device it works. Commented Jan 22, 2020 at 16:23
  • @krjw - Thank you - You are absolutely right; the NavigationLinks work perfectly on device. However, it still crashes if you try to delete the last element when 'edit in place' is on. Commented Jan 22, 2020 at 16:47
  • I know where the Index out of range happens, but I am still working on a suitable solution for you Commented Jan 22, 2020 at 16:49
  • @krjw - good luck and thank you. Commented Jan 22, 2020 at 17:02
  • 1
    @krjw - yes, I saw it a little while ago - BRILLIANT WORK! I can't thank you enough. Commented Jan 23, 2020 at 13:50

1 Answer 1

1

Ok despite my comment I tried to get to a solution and I might found an acceptable one:

I had to remodel Person... The whole indices was the issue of course but I couldn't exactly find out when what happens. I even tried with a local @State which updates the view and then updates the array of the @ObservedObject...

here are some links which could help to further investigate though...

Swift UI detail remove

How do I set the toggle state in a foreach loop in SwiftUI

Also this link here shows how to update members of an observed array generically which is pretty cool!:

https://stackoverflow.com/a/57920136/5981293

struct EditView: View {
    @ObservedObject var person: Person
    var body: some View {
        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))
        }.navigationBarTitle("Edit entry")
    }
}

struct RowView: View {
    @Binding var allowEditing: Bool
    @ObservedObject var individual: Person

    var body: some View {
        HStack {
            if self.allowEditing {
                //Toggle to edit in place
                Toggle(isOn: self.$individual.isEditable){
                    Text("edit").font(.headline).foregroundColor(.green).opacity(self.individual.isEditable ? 1.0 : 0.4)
                }//.frame(width:100)
            }

            if self.individual.isEditable{
                EditView(person: self.individual)
            }
            else{
                NavigationLink(destination:EditView(person: self.individual)){
                    Text("\(self.individual.name1) \(self.individual.name2)")
                        //.frame(width: 200, alignment: .leading)
                        .padding(.all, 3)
                }// link
            }
        }
    }
}


class Person: ObservableObject, Identifiable {
    @Published var id:UUID
    @Published var name1:String
    @Published var name2:String
    @Published var isEditable:Bool

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

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
    @State private var allowEditing:Bool = false

    // 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)")
                    }
                    Toggle(isOn: $allowEditing){Text("edit in place")}.padding(.all,5).overlay(RoundedRectangle(cornerRadius: 8)
                        .stroke(Color.red, lineWidth: 2))
                    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
                        RowView(allowEditing: self.$allowEditing, individual: 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)
    }
}

I hope this helps!

Sign up to request clarification or add additional context in comments.

5 Comments

that seems to work fine, thank you - you must have spent quite a bit of time on that. I haven't fully figured out what you did yet but I will study it. Thanks again, great work!
no problem! I have just user ObservableObject and added a RowView which observes on one Person object. The List now depends on the @State which is an Array of ObservableObjects
It certainly solved the delete issue but I think there is still some index problem. if you have 'edit in place' switched on, delete 2 elements, then add 2 more - the new elements' toggles don't work and are shifted to the left. It seems to correspond to how many are deleted.
You are right but this could be something different. I will have a look tomorrow. I have some other stuff to do unfortunately... good luck though :)
I am actually really new to Swift and I try to learn while solving these kind of questions. I mean I have encountered some of them myself, but there is still so much to learn

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.