6

I have a problem with List in SwiftUI. Removing any item from the list crashes with error Fatal error: Index out of range. It doesn't work using onDelete method and doesn't work in my custom function. What am I doing wrong?

It's macOS app, not iOS. I'm on macOS 10.15.1 and using Xcode 11.2.1.

Here is my code:


import SwiftUI

struct TodoItem: Identifiable {
    var id = UUID()
    var name: String
    var isCompleted = false
}

struct TodoRow: View {
    @Binding var todo: TodoItem
    @State var buttonHover: Bool = false
    var index: Int
    var removeTodo: (_ index: Int) -> Void

    func toggleTodo() {
        self.$todo.isCompleted.wrappedValue.toggle()
    }

    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Button(action: toggleTodo) {
                    Image(nsImage: NSImage(named: NSImage.Name(NSImage.menuOnStateTemplateName))!)
                        .resizable()
                        .frame(width: 8, height: 8)
                        .padding(3)
                        .opacity(todo.isCompleted ? 1 : buttonHover ? 0.5 : 0)
                }.buttonStyle(PlainButtonStyle())
                    .background(Capsule().stroke(Color.primary, lineWidth: 1))
                    .onHover(perform: { val  in self.buttonHover = val })
                Text("\(todo.name)").strikethrough(todo.isCompleted, color: Color.primary)
            }.opacity(todo.isCompleted ? 0.35 : 1)
            Divider().fixedSize(horizontal: false, vertical: true).frame(height: 1)
        }.contextMenu {
            Button(action: {
                self.removeTodo(self.index)
            }) {
                Text("Remove")
            }
        }
    }
}

struct TodoList: View {
    var listName: String
    @State var newTodo: String = ""
    @State var todos: [TodoItem] = []
    @State var showCompleted = false

    func addTodo() {
        let trimmedTodo = newTodo.trimmingCharacters(in: .whitespacesAndNewlines)
        if !trimmedTodo.isEmpty {
            todos.insert(TodoItem(name: trimmedTodo), at: 0)
            newTodo = ""
        }
    }

    func removeTodo(index: Int) -> Void {
        // remove is crashing the app :(
        self.todos.remove(at: index)
    }

    var body: some View {
        return VStack(alignment: .leading) {
            Text("\(listName)").font(.system(size: 20))
            HStack {
                TextField("New todo...", text: $newTodo)
                NativeButton("Add", keyEquivalent: .return) {
                    self.addTodo()
                }
            }
            List {
                ForEach(todos.indices.filter { self.showCompleted || !self.todos[$0].isCompleted }, id: \.self) { index in
                    TodoRow(todo: self.$todos[index], index: index, removeTodo: self.removeTodo)
                }.onDelete{offsets in
                    // remove is crashing the app :(
                    self.todos.remove(atOffsets: offsets)
                }
            }
            Toggle(isOn: $showCompleted) {
                Text("Show completed")
            }
        }.padding().frame(minWidth: 400, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
    }
}

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

Thanks for your help and sorry for the messy code, I'm still new to Swift and SwiftUI.

user3061425
  • 111
  • 1
  • 4
  • Does this answer your question? ['Fatal error: index out of range' when deleting bound object in view](https://stackoverflow.com/questions/58984109/fatal-error-index-out-of-range-when-deleting-bound-object-in-view) – Asperi Dec 02 '19 at 16:24
  • Unfortunately no. – user3061425 Dec 02 '19 at 19:06

2 Answers2

5

So the answer from fakiho (thank you!) was not the complete solution but it helped me find the solution. I think the problem was with @Binding var todo: TodoItem in TodoRow. I ended up passing all properties that I needed instead of whole TodoItem and it's working.

Here is full working code if anyone has the same issue:

struct TodoRow: View {
    @State var buttonHover: Bool = false
    var name: String
    var isCompleted: Bool
    var toggleItem: () -> Void
    var removeItem: () -> Void

    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Button(action: toggleItem) {
                    Image(nsImage: NSImage(named: NSImage.Name(NSImage.menuOnStateTemplateName))!)
                        .resizable()
                        .frame(width: 8, height: 8)
                        .padding(3)
                        .opacity(isCompleted ? 1 : buttonHover ? 0.5 : 0)
                }.buttonStyle(PlainButtonStyle())
                    .background(Capsule().stroke(Color.primary, lineWidth: 1))
                    .onHover(perform: { val  in self.buttonHover = val })
                Text("\(name)").strikethrough(isCompleted, color: Color.primary)
            }.opacity(isCompleted ? 0.35 : 1)
            Divider().fixedSize(horizontal: false, vertical: true).frame(height: 1)
        }.contextMenu {
            Button(action: removeItem) {
                Text("Delete")
            }
        }
    }
}

struct TodoList: View {
    var listName: String
    @State var newTodo: String = ""
    @State var todos: [TodoItem] = []
    @State var showCompleted = false

    func addTodo() {
        let trimmedTodo = newTodo.trimmingCharacters(in: .whitespacesAndNewlines)
        if !trimmedTodo.isEmpty {
            todos.insert(TodoItem(name: trimmedTodo), at: 0)
            newTodo = ""
        }
    }

    var body: some View {
        return VStack(alignment: .leading) {
            Text("\(listName)").font(.headline)
            HStack {
                TextField("New todo...", text: $newTodo)
                NativeButton("Add", keyEquivalent: .return) {
                    self.addTodo()
                }
            }
            List {
                ForEach(todos.indices.filter { self.showCompleted || !todos[$0].isCompleted }, id: \.self) { index in
                    TodoRow(
                        name: self.todos[index].name,
                        isCompleted: self.todos[index].isCompleted,
                        toggleItem: {
                            self.$todos[index].isCompleted.wrappedValue.toggle()
                        },
                        removeItem: {
                            self.todos.remove(at: index)
                        }
                    )
                }.onDelete { offsets in
                    self.todos.remove(atOffsets: offsets)
                }
            }
            Toggle(isOn: $showCompleted) {
                Text("Show completed")
            }
        }.padding().frame(minWidth: 400, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
    }
}
user3061425
  • 111
  • 1
  • 4
1

hello I've noticed that you are filtering the indices and I checked the offset on delete and I found out that sometimes the offset range is different that the actual index, I tried to change some things :

      List {
                ForEach(todos.filter { self.showCompleted || !$0.isCompleted }, id: \.self) { item in
                    TodoRow(todo: item)
                }.onDelete{offsets in
                    self.todos.remove(atOffsets: offsets)
                }
            }

and this way need to make TodoItem conform to 'Hashable' -> struct TodoItem: Identifiable, Hashable

so give it a try and hit me back if it didn't solve your problem

fakiho
  • 521
  • 4
  • 13
  • Unfortunately it doesn't work. It doesn't even compile. `ForEach(todos.filter { self.showCompleted || !$0.isCompleted }, id: \.self) { item in` causes error `Static member 'leading' cannot be used on instance of type 'HorizontalAlignment'` on the first line of `body`, even without filter (`ForEach(todos, id: \.id) { item in`). If I remove `(alignment: .leading)` it gives me an error on the next line: `Type of expression is ambiguous without more context`... – user3061425 Dec 02 '19 at 19:05
  • sometimes when the compiler gives an error similar to the one you had is because there's something Components not taking all the arguments.. make sure that you removed the required variables in TodoRow like index and removeTodo and hit me back – fakiho Dec 02 '19 at 19:26