swiftuimethodson-delete

SwiftUI onDelete() need the row ID from the IndexSet


I'm trying to use SwiftUI with onDelete() method to capture the TodoItem.id of the row that was swiped. I've set up an array called arrayOfTodosToDelete which will be the IDs of the deleted rows ie ["aaa", "ccc"]. However I'm getting ["aaa", "aaa"] and cannot figure out why. Since I need to sync upon Save over a network, I cannot use array.remove(atOffsets: offsets)

import SwiftUI

struct TodoItem: Identifiable {
    var id: String
    var todoID: String
    var todoName: String
    var isCompleted: Bool = false
}

final class TodoList: ObservableObject {
    
    @Published var items: [TodoItem] = [TodoItem]()
    @Published var appIsLoading: Bool = false
    
    var sortedTodos: [TodoItem] {
        return self.items.sorted { $0.todoName < $1.todoName }
    }
    
    func fetchTodos() {
        self.appIsLoading = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.items = [
                TodoItem(id: "aaa", todoID: "aaa", todoName: "Take Out Trash", isCompleted: false),
                TodoItem(id: "bbb", todoID: "bbb", todoName: "Clean Room", isCompleted: true),
                TodoItem(id: "ccc", todoID: "ccc", todoName: "Check Mail", isCompleted: false),
                TodoItem(id: "ddd", todoID: "ddd", todoName: "Walk The Dog", isCompleted: true)
            ]
            
            self.appIsLoading = false
        }
        
    }
}

struct TodoListView: View {
    
    var todosArray: [TodoItem]
    var appIsLoading: Bool
    
    @State private var hideCompleted: Bool = false
    @State private var todoToDelete: TodoItem?
    @State private var arrayOfTodosToDelete: [String] = [String]()
    
    private var filteredTodos: [TodoItem] {
        return self.todosArray.filter({ (todo) -> Bool in
            
            var returnVal = true
            
            if self.hideCompleted == true && todo.isCompleted == true {
                returnVal = false
            }
            
            return returnVal
             
        })
    }
    
    func deleteTodo(atOffsets indexSet: IndexSet) {
        // cannot figure out how to get the id of the swiped row and append to array arrayOfTodosToDelete
        print("at offsets \(indexSet)")
        if let index = indexSet.first {
            print("index is \(index)")
            todoToDelete = filteredTodos[index]
            print("todo To Delete \(String(describing: todoToDelete))")
            
            if let getTodo = filteredTodos.first(where: { $0.todoID == todoToDelete!.todoID }) {
                print("todo To Delete \(String(describing: todoToDelete!.todoID))")
                print("get Todo id \(getTodo.todoID)")
                arrayOfTodosToDelete.append(String(todoToDelete!.todoID))
            }
            
        }
    }
    
    func onSubmit() {
        
        print("Current Array is \(self.arrayOfTodosToDelete)")
        // here if user deleted "Take Out Trash" and "Clean Room" Im needing an array here of ["aaa", "bbb"]
        // here handle sync to database
        // however here I'm getting ["aaa", "aaa"] instead of ["aaa", "ccc"]
        
    }
    
    var body: some View {
        ZStack {
            
            List {
                
                ForEach(filteredTodos) { todo in
                    
                    Text(todo.todoName)
                    
                }.onDelete(perform: deleteTodo)
                
            }
            
            if appIsLoading { Text("Loading") }
            
        }.navigationTitle("Todos")
            .toolbar(content: {
                
                ToolbarItem(placement: .topBarLeading) {
                    
                    Button(action: {
                        withAnimation {
                            self.hideCompleted.toggle()
                        }
                        
                    }, label: {
                        
                        Text("Hide")
                        
                    })
                    
                }
                
                ToolbarItem(placement: .topBarTrailing) {
                    
                    Button(action: {
                        
                        self.onSubmit()
                        
                        
                    }, label: {
                        
                        Text("Save")
                        
                    })
                    
                }
                
            })
    }
}




struct ContentView: View {
    
    @StateObject var todosList = TodoList()

    private func getData() {
        
        self.todosList.fetchTodos()
        
    }
    
    var body: some View {
        ZStack {
            NavigationView {
                
                TodoListView(todosArray: todosList.sortedTodos, appIsLoading: todosList.appIsLoading)
                
            }
            
        }.onAppear {
            self.getData()
        }
    }
}

#Preview {
    ContentView()
}

Solution

  • The issue is that you are using the sortedTodos instead of items in

      TodoListView(todosArray: todosList.items, appIsLoading: todosList.appIsLoading)
    

    Note your code could be much better using the standard way of using ObservableObject (pass it to other views, that's what it's designed for) as decribed in Monitoring data or using the more modern approach Managing model data in your app.

    You could also try the following approach, where the [items] are sorted in place, and passing the todosList data model to the TodoListView.

    struct ContentView: View {
        @StateObject private var todosList = TodoList()
        
        var body: some View {
            ZStack {
                NavigationView {
                    TodoListView()
                }
            }
            .onAppear {
                todosList.fetchTodos()
            }
            .environmentObject(todosList)
        }
    }
    
    struct TodoItem: Identifiable {
        let id: String
        var todoID: String
        var todoName: String
        var isCompleted: Bool
    }
    
    final class TodoList: ObservableObject {
        @Published var items: [TodoItem] = []
        @Published var appIsLoading: Bool = false
        
        func fetchTodos() {
            self.appIsLoading = true
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.items = [
                    TodoItem(id: "aaa", todoID: "aaa", todoName: "Take Out Trash", isCompleted: false),
                    TodoItem(id: "bbb", todoID: "bbb", todoName: "Clean Room", isCompleted: true),
                    TodoItem(id: "ccc", todoID: "ccc", todoName: "Check Mail", isCompleted: false),
                    TodoItem(id: "ddd", todoID: "ddd", todoName: "Walk The Dog", isCompleted: true)
                ]
                self.items.sort{$0.todoName < $1.todoName} // <--- sort in place
                self.appIsLoading = false
            }
        }
    }
    
    struct TodoListView: View {
        @EnvironmentObject var todosList: TodoList
        
        @State private var hideCompleted: Bool = false
        @State private var arrayOfTodosToDelete: [String] = []
        
        private var filteredTodos: [TodoItem] {
            todosList.items.filter{ hideCompleted && $0.isCompleted ? false : true }
        }
        
        func deleteTodo(atOffsets indexSet: IndexSet) {
            if let index = indexSet.first {
                // get the index in the original todosList.items
                if let delIndex = todosList.items.firstIndex(where: { $0.todoID == filteredTodos[index].todoID }) {
                    arrayOfTodosToDelete.append(todosList.items[delIndex].todoID)
                    print("deleting: \(todosList.items[delIndex].todoID)")
                    todosList.items.remove(at: delIndex)  // <--- do the deleting
                }
            }
        }
    
        func onSubmit() {
            print("----> onSubmit arrayOfTodosToDelete is: \(self.arrayOfTodosToDelete)")
        }
        
        var body: some View {
            ZStack {
                List {
                    ForEach(filteredTodos) { todo in
                        Text(todo.todoName)
                    }.onDelete(perform: deleteTodo)
                }
                if todosList.appIsLoading { Text("Loading") }
            }
            .navigationTitle("Todos")
            .toolbar(content: {
                ToolbarItem(placement: .topBarLeading) {
                    Button("Hide") {
                        withAnimation {
                            hideCompleted.toggle()
                        }
                    }
                }
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Save") {
                        onSubmit()
                    }
                }
            })
        }
    }
    

    EDIT-1:

    If you don't want to actually remove the selected items in deleteTodo, but just record them, try this example code using an extra filter in the filteredTodos. This will keep the List displayed in sync with the data selections, and no items are deleted, presumably until you do it in onSubmit.

    struct TodoListView: View {
        @EnvironmentObject var todosList: TodoList
        
        @State private var hideCompleted: Bool = false
        @State private var arrayOfTodosToDelete: [String] = []
    
        private var filteredTodos: [TodoItem] {
            todosList.items
                .sorted(by: {$0.todoName < $1.todoName}) // <--- here
                .filter{ hideCompleted && $0.isCompleted ? false : true }
                .filter{ !arrayOfTodosToDelete.contains($0.todoID) } // <--- here
        }
        
        func deleteTodo(atOffsets indexSet: IndexSet) {
            if let index = indexSet.first {
                // get the index in the original todosList.items
                if let delIndex = todosList.items.firstIndex(where: { $0.todoID == filteredTodos[index].todoID }) {
                    arrayOfTodosToDelete.append(todosList.items[delIndex].todoID) // <--- here
                    print("recording: \(todosList.items[delIndex].todoID) for deletion")
                }
            }
        }
    
        func onSubmit() {
            print("----> onSubmit arrayOfTodosToDelete is: \(arrayOfTodosToDelete)")
            //.... do your save/delete over the network
        }
        
        var body: some View {
            ZStack {
                List {
                    ForEach(filteredTodos) { todo in
                        Text(todo.todoName)
                    }.onDelete(perform: deleteTodo)
                }
                if todosList.appIsLoading { Text("Loading") }
            }
            .navigationTitle("Todos")
            .toolbar(content: {
                ToolbarItem(placement: .topBarLeading) {
                    Button("Hide") {
                        withAnimation {
                            hideCompleted.toggle()
                        }
                    }
                }
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Save") {
                        onSubmit()
                    }
                }
            })
        }
    }
    
    final class TodoList: ObservableObject {
        @Published var items: [TodoItem] = []
        @Published var appIsLoading: Bool = false
        
        func fetchTodos() {
            self.appIsLoading = true
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.items = [
                    TodoItem(id: "aaa", todoID: "aaa", todoName: "Take Out Trash", isCompleted: false),
                    TodoItem(id: "bbb", todoID: "bbb", todoName: "Clean Room", isCompleted: true),
                    TodoItem(id: "ccc", todoID: "ccc", todoName: "Check Mail", isCompleted: false),
                    TodoItem(id: "ddd", todoID: "ddd", todoName: "Walk The Dog", isCompleted: true)
                ]
                self.appIsLoading = false
            }
        }
    }