From 8956b782c3469a120982f6c1be29ccc22585bb5e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 11 Feb 2026 16:26:21 +0100 Subject: [PATCH] Added support the user reordering of todo tasks for the TaskListView view in the app target. --- .../View Models/TaskListViewModel.swift | 46 +++++++++++++--- App/Sources/Views/TaskListView.swift | 10 +++- .../View Models/TaskListViewModelTests.swift | 55 ++++++++++++++++++- 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/App/Sources/View Models/TaskListViewModel.swift b/App/Sources/View Models/TaskListViewModel.swift index 2db3eb9..ac92387 100644 --- a/App/Sources/View Models/TaskListViewModel.swift +++ b/App/Sources/View Models/TaskListViewModel.swift @@ -15,17 +15,12 @@ extension TaskListView { /// - Provides the localized title for the navigation bar /// - Determines whether the completion filter toggle should be displayed /// - Provides label and icon configuration for the completion toggle button + /// - Handles task reordering through drag and drop interactions @Observable final class ViewModel { // MARK: Properties - /// The task group to display tasks for. - /// - /// This property determines which tasks are fetched and displayed in the list. - /// It is used by the content view to initialize the SwiftData query with the appropriate predicates and sort descriptors for the group. - let group: TodoGroup - /// Whether to show completed tasks in the list. /// /// This property controls the inclusion of completed tasks in the query results: @@ -33,6 +28,13 @@ extension TaskListView { /// - `false`: Completed tasks are filtered out at the database level var showCompleted: Bool + /// The task group to display tasks for. + /// + /// This property determines which tasks are fetched and displayed in the list. + /// It is used by the content view to initialize the SwiftData query with the appropriate + /// predicates and sort descriptors for the group. + let group: TodoGroup + // MARK: Initialization /// Initializes a new view model instance for a specific task group. @@ -40,7 +42,7 @@ extension TaskListView { /// - Parameter group: The task group to display tasks for. init(group: TodoGroup) { self.group = group - showCompleted = false + self.showCompleted = false } // MARK: Computed Properties @@ -82,6 +84,36 @@ extension TaskListView { // MARK: Methods + /// Reorders tasks after a drag and drop operation. + /// + /// This method is called when users drag and drop tasks to reorder them within a group. + /// It updates the `order` property of all tasks to reflect their new positions, ensuring + /// that the user's manual ordering preference is persisted. + /// + /// - Parameters: + /// - tasks: The current array of tasks before reordering. + /// - source: The index set of items being moved (typically a single index). + /// - destination: The destination index where items should be inserted. + /// - Note: This method should only be called for groups where `canReorder` is `true`. + /// The view layer is responsible for only enabling drag and drop for applicable groups. + /// - SeeAlso: ``TodoGroup/canReorder`` for determining which groups support reordering. + func reorderTasks( + _ tasks: [Todo], + from source: IndexSet, + to destination: Int + ) { + var updatedTasks = tasks + + updatedTasks.move( + fromOffsets: source, + toOffset: destination + ) + + for (index, task) in updatedTasks.enumerated() { + task.order = index + } + } + /// Toggles the visibility of completed tasks. /// /// This method flips the `showCompleted` state, which triggers SwiftUI to recreate the task list view and its children. diff --git a/App/Sources/Views/TaskListView.swift b/App/Sources/Views/TaskListView.swift index e8eb57c..9794943 100644 --- a/App/Sources/Views/TaskListView.swift +++ b/App/Sources/Views/TaskListView.swift @@ -19,7 +19,13 @@ struct TaskListView: View { /// Creates a new task list view for a specific group. /// + /// Initializes the view with a view model configured for the specified group. + /// The view model manages the state for completion filtering and task reordering, + /// while the view handles the presentation layer including navigation and toolbar. + /// /// - Parameter group: The todo group category to display tasks for. + /// - Note: The group determines which tasks are fetched, whether + /// completion filtering is available, and whether drag and drop reordering is enabled. init(group: TodoGroup) { _viewModel = .init(initialValue: ViewModel(group: group)) } @@ -34,7 +40,9 @@ struct TaskListView: View { Content( group: viewModel.group, showCompleted: viewModel.showCompleted - ) + ) { tasks, source, destination in + viewModel.reorderTasks(tasks, from: source, to: destination) + } .navigationTitle(viewModel.navigationTitle) .toolbar { if viewModel.canShowCompleted { diff --git a/UnitTests/Tests/View Models/TaskListViewModelTests.swift b/UnitTests/Tests/View Models/TaskListViewModelTests.swift index c87cf5c..a1e98cf 100644 --- a/UnitTests/Tests/View Models/TaskListViewModelTests.swift +++ b/UnitTests/Tests/View Models/TaskListViewModelTests.swift @@ -36,7 +36,7 @@ struct TaskListViewModelTests { ) { // GIVEN let viewModel = TaskListView.ViewModel(group: group) - + viewModel.showCompleted = showCompleted // WHEN @@ -46,7 +46,8 @@ struct TaskListViewModelTests { if showCompleted { #expect(title == "view.task-list.label.hide-completed") #expect(icon == "eye.slash") - } else { + } + else { #expect(title == "view.task-list.label.show-completed") #expect(icon == "eye") } @@ -90,6 +91,56 @@ struct TaskListViewModelTests { // MARK: Method tests + @Test("reorderTasks updates order property when moving task forward") + func reorderTasksMovingForward() { + // GIVEN + let viewModel = TaskListView.ViewModel(group: .today) + let tasks: [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), + .init(title: "Task 4", due: .now, order: 3), + ] + + // WHEN + viewModel.reorderTasks( + tasks, + from: IndexSet(integer: 0), + to: 3 + ) + + // THEN + #expect(tasks[0].order == 2) + #expect(tasks[1].order == 0) + #expect(tasks[2].order == 1) + #expect(tasks[3].order == 3) + } + + @Test("reorderTasks updates order property when moving task backward") + func reorderTasksMovingBackward() { + // GIVEN + let viewModel = TaskListView.ViewModel(group: .today) + let tasks: [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), + .init(title: "Task 4", due: .now, order: 3), + ] + + // WHEN + viewModel.reorderTasks( + tasks, + from: IndexSet(integer: 3), + to: 1 + ) + + // THEN + #expect(tasks[0].order == 0) + #expect(tasks[1].order == 2) + #expect(tasks[2].order == 3) + #expect(tasks[3].order == 1) + } + @Test("toggleCompletedVisbility toggles the showCompleted property") func toggleCompletedVisibility() { // GIVEN