1

I'm giving up. Here's the problem: I try to load an array names as an ObservableObject in my List. The names must be connected as Bindings to the TextField:

class Users: ObservableObject {
    @Published var names = ["Adele", "Taylor", "Michael"]
}

My ContentView looks like this:

@EnvironmentObject var users: Users
var body: some View {
    NavigationView {
        List {
            ForEach(0..<users.names.count, id: \.self) {
                TextField("titel", text: $users.names[$0])
            }
            .onDelete(perform: delete)
        }
        .navigationBarItems(trailing: EditButton())
        .navigationTitle("bug")
    }
}
func delete(at offsets: IndexSet) {
    users.names.remove(atOffsets: offsets)
}

The environmentObject is linked to the ContentView():

ContentView()
.environmentObject(Users())

Everything works fine, but when I try to delete a row, it looks funny: The row below slides to the right and the row which should be deleted moves upwards. It's even not consistent but depends on which item I start deleting on. See this .gif:

Bug in SwiftUI List

What I've tried so far:

I think the bug has something to do with the list not being Identifiable. So I tried using something like:

ForEach(users.names, id: \.id) {
    ...
}

With a new Users class with an id: UUID() But using the objects themselves and not 0..<users.names.count only leads to the problem, that I can't use a Binding in my ForEach-Loop. See also: @Binding and ForEach in SwiftUI

So my questions are:

  • Do I miss something here? I'm new to SwiftUI so there's a big chance I don't get a fundamentally thing.
  • Can I use a Binding in an ForEach-Loop while also using an UUID as an id (not: id: \.self) ?
  • Can I make the items in an ForEach-Loop so identifiable, that there are no visual bugs when deleting them while also using ForEach(0..<users.names.count, id: \.self) ?
J. Mann
  • 115
  • 6

2 Answers2

1

You can use enumerated to access both the element and its index:

ForEach(Array(users.names.enumerated()), id: \.element.id) { index, element in
    // use either `index` or `element` here
}

You can also use this extension to make sure you don't access non-existing bindings when deleting items.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
1

With the help of @pawello and @George_E my working code now looks like this:

struct UserWithUUID: Identifiable {
    var id: UUID
    var name: String
}

class Users: ObservableObject {
    @Published var names = [UserWithUUID(id: UUID(), name: "Taylor"), UserWithUUID(id: UUID(), name: "Swift")]
}

struct ContentView: View {
    @EnvironmentObject var users: Users
    
    var body: some View {
        NavigationView {
            List {
                ForEach(Array(users.names.enumerated()), id: \.element.id) { index, element in
                    TextField("titel", text: Binding<String>(get: { element.name }, set: { users.names[index].name = $0 }))
                }
                .onDelete(perform: delete)
            }
            .navigationBarItems(trailing: EditButton())
            .navigationTitle("working")
        }
    }
    func delete(at offsets: IndexSet) {
        users.names.remove(atOffsets: offsets)
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
J. Mann
  • 115
  • 6