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.

  • The single use bug does not happen on device. Just tested it in Simulator and on my device and on device it works. – krjw Jan 22 '20 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. – philslinkee Jan 22 '20 at 16:47
  • I know where the Index out of range happens, but I am still working on a suitable solution for you – krjw Jan 22 '20 at 16:49
  • @krjw - good luck and thank you. – philslinkee Jan 22 '20 at 17:02
  • So it is definitely the line involving the following statement (where the toggle is in the list): `self.$people.individuals[self.people.individuals.firstIndex(of:individual)!].isEditable` maybe make `Person` an `ObservableObject` so you can do something like `individual.$isEditable`... I will try some stuff later but I have to do something else now sorry – krjw Jan 22 '20 at 17:13
  • see https://stackoverflow.com/questions/59859178/swift-ui-detail-remove/59864751?noredirect=1#comment105861720_59864751 Don't use idex in array as an identity of data! – user3441734 Jan 22 '20 at 17:45
  • @krjw - Thank you for taking the time to try and help me. I'll play around with it a bit more, maybe I'll have a go at making Person an ObservableObject, like you suggest. – philslinkee Jan 23 '20 at 09:55
  • @user3441734 - I'll have a good look at that a bit later, thank you. – philslinkee Jan 23 '20 at 11:02
  • @krjw - something else I noticed while playing around is that if the Toggle is visible in the last element, or the fields are editable then deleting that last element will crash. So, if there's a visible active binding to the underlying array in the last element, deleting it will cause 'Fatal error: Index out of range'. To see what I mean, add a few people to the list, switch 'edit in place', switch the toggle on say the 3rd from last element, switch 'edit in place' off, then start deleting elements from the bottom. it won't crash until you reach the element with blue fields. – philslinkee Jan 23 '20 at 11:30
  • yeah I also found some stuff about the active binding issue... I tried workarounds but had no luck... yours sounds good! – krjw Jan 23 '20 at 11:32
  • @philslinkee I don't think I am of any help here... I might have to give up for now... there are more pressing matters. I might return to this later. Good luck though! You might extract this problem and post it as a separate problem!!! – krjw Jan 23 '20 at 11:46
  • I tried making Person an ObservableObject by changing the struct to a class with the same basic properties, however, the compiler then says Person is not Equatable, not sure where to go then. – philslinkee Jan 23 '20 at 12:14
  • @philslinkee `Person` must also implement the `Equatable` Protocol ... plus add this function: `static func == (lhs: Person, rhs: Person) -> Bool { return lhs.id == rhs.id }` – krjw Jan 23 '20 at 12:16
  • @krjw - I really appreciate you having a look, thank you. I may have to give up on the idea of 'editing in place' - shame really because I think that would be a widely usable bit of code. – philslinkee Jan 23 '20 at 12:17
  • @philslinkee have you looked at the answer I provided below? – krjw Jan 23 '20 at 12:17
  • 1
    @krjw - yes, I saw it a little while ago - BRILLIANT WORK! I can't thank you enough. – philslinkee Jan 23 '20 at 13:50

1 Answers1

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!

krjw
  • 4,070
  • 1
  • 24
  • 49
  • 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! – philslinkee Jan 23 '20 at 13:39
  • 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` – krjw Jan 23 '20 at 15:20
  • 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. – philslinkee Jan 23 '20 at 16:48
  • 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 :) – krjw Jan 23 '20 at 17:03
  • 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 – krjw Jan 23 '20 at 17:09