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.
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)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With