From 8324a64b24a8abf863f0ec3d3207f2e3aae234d7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 17:33:22 +0100 Subject: [PATCH 1/2] Implemented the "deleteTasks(_: at: from: )" function for the LastListViewModel view model in the app target. --- .../View Models/TaskListViewModel.swift | 21 ++++++++++ .../View Models/TaskListViewModelTests.swift | 39 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/App/Sources/View Models/TaskListViewModel.swift b/App/Sources/View Models/TaskListViewModel.swift index ac92387..3eec5fc 100644 --- a/App/Sources/View Models/TaskListViewModel.swift +++ b/App/Sources/View Models/TaskListViewModel.swift @@ -1,3 +1,4 @@ +import Foundation import SwiftData import SwiftUI @@ -84,6 +85,26 @@ extension TaskListView { // MARK: Methods + /// Deletes tasks at the specified indices. + /// + /// This method removes tasks from the model context after a swipe-to-delete action. + /// The deletion is performed through SwiftData's model context, which handles + /// persistence and triggers automatic UI updates. + /// + /// - Parameters: + /// - tasks: The current array of tasks. + /// - offsets: The index set of tasks to delete. + /// - modelContext: The SwiftData model context to perform the deletion. + func deleteTasks( + _ tasks: [Todo], + at offsets: IndexSet, + from modelContext: ModelContext + ) { + for index in offsets { + modelContext.delete(tasks[index]) + } + } + /// Reorders tasks after a drag and drop operation. /// /// This method is called when users drag and drop tasks to reorder them within a group. diff --git a/UnitTests/Tests/View Models/TaskListViewModelTests.swift b/UnitTests/Tests/View Models/TaskListViewModelTests.swift index a1e98cf..619409b 100644 --- a/UnitTests/Tests/View Models/TaskListViewModelTests.swift +++ b/UnitTests/Tests/View Models/TaskListViewModelTests.swift @@ -91,6 +91,45 @@ struct TaskListViewModelTests { // MARK: Method tests + @Test("deleteTasks removes a single task from model context") + func deleteTasksSingle() throws { + // GIVEN + let container = try ModelContainer( + for: Todo.self, + configurations: .init(isStoredInMemoryOnly: true) + ) + let context = ModelContext(container) + + let viewModel = TaskListView.ViewModel(group: .today) + let todosToInsert: [Todo] = [ + .init(title: "Task 1", due: .now, order: 0), + .init(title: "Task 2", due: .now, order: 1), + .init(title: "Task 3", due: .now, order: 2), + ] + let countInitial = todosToInsert.count + + for todo in todosToInsert { + context.insert(todo) + } + + // WHEN + viewModel.deleteTasks( + todosToInsert, + at: IndexSet(integer: 1), + from: context + ) + + // THEN + let descriptor = FetchDescriptor() + let todos = try context.fetch(descriptor) + + #expect(countInitial == 3) + #expect(todos.count == 2) + #expect(todos.contains(where: { $0.title == "Task 1" })) + #expect(todos.contains(where: { $0.title == "Task 3" })) + #expect(!todos.contains(where: { $0.title == "Task 2" })) + } + @Test("reorderTasks updates order property when moving task forward") func reorderTasksMovingForward() { // GIVEN -- 2.54.0 From 1debdcd32294071b734529eb3bcff34f709abb89 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 17:34:06 +0100 Subject: [PATCH 2/2] Added support for todo deletion to the TaskListView view in the app target. --- .../Views/TaskList/TaskListContent.swift | 84 ++++++++++++++----- App/Sources/Views/TaskListView.swift | 18 +++- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/App/Sources/Views/TaskList/TaskListContent.swift b/App/Sources/Views/TaskList/TaskListContent.swift index f918ad2..d093caa 100644 --- a/App/Sources/Views/TaskList/TaskListContent.swift +++ b/App/Sources/Views/TaskList/TaskListContent.swift @@ -18,17 +18,24 @@ extension TaskListView { // MARK: Type aliases + /// Closure type for handling task deletion operations. + /// + /// - Parameters: + /// - todos: The current array of todo tasks. + /// - offsets: The index set of tasks to delete. + typealias DeleteClosure = ([Todo], IndexSet) -> Void + /// Closure type for handling task reorder operations. /// /// - Parameters: - /// - tasks: The current array of tasks. + /// - todos: The current array of todo tasks. /// - source: The index set of items being moved. /// - destination: The destination index for the moved items. typealias ReorderClosure = ([Todo], IndexSet, Int) -> Void // MARK: Properties - /// Query to fetch todos filtered by group and completion status. + /// Query to fetch todo tasks filtered by group and completion status. /// /// This query is initialized with a todo fetch descriptor that applies predicates /// based on the group type and completion filter. SwiftData automatically @@ -37,7 +44,7 @@ extension TaskListView { /// Tasks are sorted by their `order` property first, then by date-based criteria /// specific to each group, enabling both manual and automatic ordering. @Query - private var tasks: [Todo] + private var todos: [Todo] /// Indicates whether tasks in this view can be reordered through drag and drop. /// @@ -45,6 +52,12 @@ extension TaskListView { /// determines whether the `.onMove` modifier is applied to the task list. private let canReorder: Bool + /// Closure called when tasks are deleted through swipe-to-delete. + /// + /// This closure is invoked by the `.onDelete` modifier and is responsible for + /// removing the selected tasks from the model context. + private let onDelete: DeleteClosure + /// Closure called when tasks are reordered through drag and drop. /// /// This closure is invoked by the `.onMove` modifier and is responsible for @@ -67,12 +80,14 @@ extension TaskListView { /// - group: The todo group category to filter by (today, upcoming, overdue, or completed). /// - showCompleted: Whether to include completed tasks in the results. Only applies to groups other than `.completed`. /// - onReorder: A closure called when tasks are reordered. + /// - onDelete: A closure called when tasks are deleted. init( group: TodoGroup, showCompleted: Bool, + onDelete: @escaping DeleteClosure, onReorder: @escaping ReorderClosure ) { - self._tasks = .init( + self._todos = .init( FetchDescriptor( group: group, showCompleted: showCompleted @@ -80,6 +95,7 @@ extension TaskListView { ) self.canReorder = group.canReorder + self.onDelete = onDelete self.onReorder = onReorder } @@ -97,7 +113,7 @@ extension TaskListView { /// in read-only mode with chronological sorting. var body: some View { Group { - if tasks.isEmpty { + if todos.isEmpty { ContentUnavailableView( "view.task-list-content.content-unavailable.title", systemImage: "checkmark.circle" @@ -105,19 +121,7 @@ extension TaskListView { } else { List { - if canReorder { - ForEach(tasks) { - TaskListView.Item($0) - } - .onMove { source, destination in - onReorder(tasks, source, destination) - } - } - else { - ForEach(tasks) { - TaskListView.Item($0) - } - } + items } .listStyle(.grouped) } @@ -127,6 +131,34 @@ extension TaskListView { } +// MARK: - Helpers + +private extension TaskListView.Content { + + // MARK: Computed + + /// A view that renders the task items with appropriate modifiers. + @ViewBuilder + var items: some View { + let items = ForEach(todos) { + TaskListView.Item($0) + } + .onDelete { offsets in + onDelete(todos, offsets) + } + + if canReorder { + items.onMove { source, destination in + onReorder(todos, source, destination) + } + } + else { + items + } + } + +} + // MARK: - Previews #Preview( @@ -136,7 +168,9 @@ extension TaskListView { TaskListView.Content( group: .today, showCompleted: .random() - ) { _, _, _ in + ) { _, _ in + // On delete closure + } onReorder: { _, _, _ in // On reorder closure } } @@ -148,7 +182,9 @@ extension TaskListView { TaskListView.Content( group: .today, showCompleted: false - ) { _, _, _ in + ) { _, _ in + // On delete closure + } onReorder: { _, _, _ in // On reorder closure } } @@ -160,7 +196,9 @@ extension TaskListView { TaskListView.Content( group: .today, showCompleted: true - ) { _, _, _ in + ) { _, _ in + // On delete closure + } onReorder: { _, _, _ in // On reorder closure } } @@ -172,7 +210,9 @@ extension TaskListView { TaskListView.Content( group: .completed, showCompleted: .random() - ) { _, _, _ in + ) { _, _ in + // On delete closure + } onReorder: { _, _, _ in // On reorder closure } } diff --git a/App/Sources/Views/TaskListView.swift b/App/Sources/Views/TaskListView.swift index 9794943..955ec32 100644 --- a/App/Sources/Views/TaskListView.swift +++ b/App/Sources/Views/TaskListView.swift @@ -9,6 +9,10 @@ struct TaskListView: View { // MARK: Properties + /// The SwiftData model context for performing data operations. + @Environment(\.modelContext) + private var modelContext + /// The view model managing the state and business logic for this view. /// /// This view model holds the group configuration and completion filter state. @@ -40,8 +44,18 @@ struct TaskListView: View { Content( group: viewModel.group, showCompleted: viewModel.showCompleted - ) { tasks, source, destination in - viewModel.reorderTasks(tasks, from: source, to: destination) + ) { tasks, offsets in + viewModel.deleteTasks( + tasks, + at: offsets, + from: modelContext + ) + } onReorder: { tasks, source, destination in + viewModel.reorderTasks( + tasks, + from: source, + to: destination + ) } .navigationTitle(viewModel.navigationTitle) .toolbar { -- 2.54.0