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()
}
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
}
}
}