Manual task reordering feature #6

Merged
javier merged 5 commits from app/reorder-tasks into main 2026-02-11 15:28:23 +00:00
3 changed files with 101 additions and 10 deletions
Showing only changes of commit 8956b782c3 - Show all commits
@@ -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.
+9 -1
View File
@@ -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 {
@@ -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