Manual task reordering feature #6
@@ -5,16 +5,6 @@ import SwiftUI
|
||||
///
|
||||
/// This enumeration provides a semantic grouping mechanism for organizing and filtering todo tasks throughout the application.
|
||||
/// Each case represents a distinct category that helps users manage their tasks based on temporal relevance and completion state.
|
||||
///
|
||||
/// ## Features
|
||||
/// - Provides localized titles and icons for each group
|
||||
/// - Supports filtering of completed tasks within temporal groups
|
||||
/// - Enables color-coded visual categorization
|
||||
///
|
||||
/// ## Groups
|
||||
/// Groups are categorized into two types:
|
||||
/// - **Temporal groups** (`.today`, `.upcoming`, `.overdue`): Time-based categorization that can optionally include completed tasks
|
||||
/// - **Status group** (`.completed`): Contains all completed tasks regardless of due date
|
||||
enum TodoGroup {
|
||||
/// Tasks that have been marked as done, regardless of due date.
|
||||
case completed
|
||||
@@ -33,14 +23,19 @@ extension TodoGroup {
|
||||
/// Indicates whether this group supports toggling the visibility of completed tasks.
|
||||
///
|
||||
/// This property determines if a show/hide completed tasks action should be available for the group.
|
||||
///
|
||||
/// - Returns: `true` for temporal groups (`.today`, `.upcoming`, `.overdue`) that can optionally
|
||||
/// show or hide completed tasks. Returns `false` for the `.completed` group since it exclusively
|
||||
/// displays completed tasks.
|
||||
var canShowCompleted: Bool {
|
||||
self != .completed
|
||||
}
|
||||
|
||||
/// Indicates whether tasks in this group can be reordered through drag and drop.
|
||||
///
|
||||
/// This property determines if the user should be able to manually reorder tasks within the group.
|
||||
/// Reordering is useful for temporal groups where users may want to prioritize certain tasks,
|
||||
/// but is disabled for completed tasks since they are typically sorted by completion date.
|
||||
var canReorder: Bool {
|
||||
self != .completed
|
||||
}
|
||||
|
||||
/// The color associated with the task group.
|
||||
var color: Color {
|
||||
switch self {
|
||||
|
||||
@@ -13,9 +13,12 @@ extension FetchDescriptor where T == Todo {
|
||||
/// relevance and completion status. Each group applies appropriate predicates and sort orders:
|
||||
///
|
||||
/// - **Completed**: All tasks marked as done, sorted by completion date (most recent first)
|
||||
/// - **Overdue**: Incomplete tasks with due dates before today, sorted by due date (most recent first)
|
||||
/// - **Today**: Incomplete tasks due today, sorted by due date (earliest first)
|
||||
/// - **Upcoming**: Incomplete tasks due after today, sorted by due date (earliest first)
|
||||
/// - **Overdue**: Incomplete tasks with due dates before today, sorted by user-defined order, then by due date (most recent first)
|
||||
/// - **Today**: Incomplete tasks due today, sorted by user-defined order, then by due date (earliest first)
|
||||
/// - **Upcoming**: Incomplete tasks due after today, sorted by user-defined order, then by due date (earliest first)
|
||||
///
|
||||
/// The user-defined order allows tasks to be manually reordered within each group through drag and drop,
|
||||
/// while maintaining a secondary sort by date for tasks with the same order value.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - group: The todo group category to filter by.
|
||||
@@ -52,11 +55,11 @@ extension FetchDescriptor where T == Todo {
|
||||
case .completed:
|
||||
[.init(\Todo.completed, order: .reverse)]
|
||||
case .overdue:
|
||||
[.init(\Todo.due, order: .reverse)]
|
||||
[.init(\Todo.order, order: .forward), .init(\Todo.due, order: .reverse)]
|
||||
case .today:
|
||||
[.init(\Todo.due, order: .forward)]
|
||||
[.init(\Todo.order, order: .forward), .init(\Todo.due, order: .forward)]
|
||||
case .upcoming:
|
||||
[.init(\Todo.due, order: .forward)]
|
||||
[.init(\Todo.order, order: .forward), .init(\Todo.due, order: .forward)]
|
||||
}
|
||||
|
||||
self.init(
|
||||
|
||||
@@ -16,6 +16,9 @@ final class Todo {
|
||||
/// An optional note providing additional details about the todo task.
|
||||
var note: String?
|
||||
|
||||
/// The display order of the todo task.
|
||||
var order: Int
|
||||
|
||||
/// The title of the todo task.
|
||||
var title: String
|
||||
|
||||
@@ -27,15 +30,18 @@ final class Todo {
|
||||
/// - note: An optional note providing additional details.
|
||||
/// - due: The date by which the todo should be completed.
|
||||
/// - completed: The date when the todo was completed, if applicable.
|
||||
/// - order: The display order of the todo task.
|
||||
init(
|
||||
title: String,
|
||||
note: String? = nil,
|
||||
due: Date,
|
||||
completed: Date? = nil
|
||||
completed: Date? = nil,
|
||||
order: Int = 0
|
||||
) {
|
||||
self.completed = completed
|
||||
self.due = due
|
||||
self.note = note
|
||||
self.order = order
|
||||
self.title = title
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -5,42 +5,82 @@ extension TaskListView {
|
||||
|
||||
/// Internal content view that manages the dynamic SwiftData query for tasks.
|
||||
///
|
||||
/// This view is responsible for querying and displaying the list of tasks based on
|
||||
/// the specified group and completion status filter. It exists as a separate view
|
||||
/// to enable dynamic query updates when filter parameters change.
|
||||
/// This view is responsible for querying and displaying the list of tasks based on the specified
|
||||
/// group and completion status filter. It exists as a separate view to enable dynamic query
|
||||
/// updates when filter parameters change.
|
||||
///
|
||||
/// Task reordering is enabled based on the ``TodoGroup/canReorder`` property.
|
||||
/// Groups that support reordering (``TodoGroup/today``, ``TodoGroup/upcoming``,
|
||||
/// ``TodoGroup/overdue``) allow users to drag and drop tasks to change their order.
|
||||
/// The ``TodoGroup/completed`` group does not support reordering as tasks are
|
||||
/// displayed in chronological order by completion date.
|
||||
struct Content: View {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
/// Closure type for handling task reorder operations.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tasks: The current array of 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.
|
||||
///
|
||||
/// This query is initialized with a `FetchDescriptor` that applies predicates
|
||||
/// This query is initialized with a todo fetch descriptor that applies predicates
|
||||
/// based on the group type and completion filter. SwiftData automatically
|
||||
/// monitors changes and updates the view when the underlying data changes.
|
||||
///
|
||||
/// 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]
|
||||
|
||||
/// Indicates whether tasks in this view can be reordered through drag and drop.
|
||||
///
|
||||
/// This value is derived from the ``TodoGroup/canReorder`` property and
|
||||
/// determines whether the `.onMove` modifier is applied to the task list.
|
||||
private let canReorder: Bool
|
||||
|
||||
/// Closure called when tasks are reordered through drag and drop.
|
||||
///
|
||||
/// This closure is invoked by the `.onMove` modifier and is responsible for
|
||||
/// updating the `order` property of affected tasks to reflect their new positions.
|
||||
private let onReorder: ReorderClosure
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a content view with a configured task query.
|
||||
/// Creates a content view with a configured task query and reorder handler.
|
||||
///
|
||||
/// This initializer sets up the `@Query` with a fetch descriptor that filters
|
||||
/// This initializer sets up a query with a fetch descriptor that filters
|
||||
/// tasks based on the specified group and completion status. When these parameters
|
||||
/// change, SwiftUI recreates the view with new values, causing the query to update.
|
||||
///
|
||||
/// The reordering capability is automatically determined based on the
|
||||
/// ``TodoGroup/canReorder`` property. For groups that support reordering,
|
||||
/// the provided closure will be called when users drag and drop tasks to new positions.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - 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.
|
||||
init(
|
||||
group: TodoGroup,
|
||||
showCompleted: Bool
|
||||
showCompleted: Bool,
|
||||
onReorder: @escaping ReorderClosure
|
||||
) {
|
||||
_tasks = .init(
|
||||
self._tasks = .init(
|
||||
FetchDescriptor<Todo>(
|
||||
group: group,
|
||||
showCompleted: showCompleted
|
||||
)
|
||||
)
|
||||
|
||||
self.canReorder = group.canReorder
|
||||
self.onReorder = onReorder
|
||||
}
|
||||
|
||||
// MARK: Body
|
||||
@@ -49,6 +89,12 @@ extension TaskListView {
|
||||
///
|
||||
/// Displays either a content unavailable state or a list of tasks
|
||||
/// based on whether any tasks match the current filters.
|
||||
///
|
||||
/// When tasks are present, displays them in a grouped list. The list supports
|
||||
/// drag and drop reordering for groups where `canReorder` is true, allowing
|
||||
/// users to manually prioritize tasks within the group. For groups where
|
||||
/// reordering is disabled (such as completed tasks), the list is displayed
|
||||
/// in read-only mode with chronological sorting.
|
||||
var body: some View {
|
||||
Group {
|
||||
if tasks.isEmpty {
|
||||
@@ -59,8 +105,18 @@ extension TaskListView {
|
||||
}
|
||||
else {
|
||||
List {
|
||||
ForEach(tasks) { todo in
|
||||
TaskListView.Item(todo)
|
||||
if canReorder {
|
||||
ForEach(tasks) {
|
||||
TaskListView.Item($0)
|
||||
}
|
||||
.onMove { source, destination in
|
||||
onReorder(tasks, source, destination)
|
||||
}
|
||||
}
|
||||
else {
|
||||
ForEach(tasks) {
|
||||
TaskListView.Item($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.grouped)
|
||||
@@ -80,7 +136,9 @@ extension TaskListView {
|
||||
TaskListView.Content(
|
||||
group: .today,
|
||||
showCompleted: .random()
|
||||
)
|
||||
) { _, _, _ in
|
||||
// On reorder closure
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(
|
||||
@@ -90,7 +148,9 @@ extension TaskListView {
|
||||
TaskListView.Content(
|
||||
group: .today,
|
||||
showCompleted: false
|
||||
)
|
||||
) { _, _, _ in
|
||||
// On reorder closure
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(
|
||||
@@ -100,5 +160,19 @@ extension TaskListView {
|
||||
TaskListView.Content(
|
||||
group: .today,
|
||||
showCompleted: true
|
||||
)
|
||||
) { _, _, _ in
|
||||
// On reorder closure
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(
|
||||
"Task list content when only completed tasks shown",
|
||||
traits: .sampleTasks
|
||||
) {
|
||||
TaskListView.Content(
|
||||
group: .completed,
|
||||
showCompleted: .random()
|
||||
) { _, _, _ in
|
||||
// On reorder closure
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -28,6 +28,25 @@ struct TodoGroupTests {
|
||||
#expect(canShowCompleted == expectation)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"canReorder returns correct values",
|
||||
arguments: zip(
|
||||
TodoGroup.allCases,
|
||||
[true, true, true, false]
|
||||
)
|
||||
)
|
||||
func canReorder(
|
||||
for group: TodoGroup,
|
||||
expects expectation: Bool
|
||||
) {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
let canReorder = group.canReorder
|
||||
|
||||
// THEN
|
||||
#expect(canReorder == expectation)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"group color match expected values",
|
||||
arguments: zip(
|
||||
|
||||
+31
-22
@@ -4,13 +4,13 @@ import Testing
|
||||
|
||||
@testable import Todos
|
||||
|
||||
@Suite("FetchDescriptor<Todo> extension tests")
|
||||
struct TodoFetchDescriptorTests {
|
||||
@Suite("TodoFetchDescriptor+Inits extension tests")
|
||||
struct TodoFetchDescriptor_InitsTests {
|
||||
|
||||
// MARK: Initialization tests
|
||||
|
||||
@Test(
|
||||
"initializes with group when empty tasks",
|
||||
"initialization when empty tasks",
|
||||
arguments: TodoGroup.allCases
|
||||
)
|
||||
@MainActor
|
||||
@@ -31,11 +31,11 @@ struct TodoFetchDescriptorTests {
|
||||
}
|
||||
|
||||
@Test(
|
||||
"initializes with group and show completed disabled when filled tasks",
|
||||
"initialization with show completed disabled",
|
||||
arguments: TodoGroup.allCases
|
||||
)
|
||||
@MainActor
|
||||
func initializationWhenFilledTasks(for group: TodoGroup) throws {
|
||||
func initializationWithNoShowCompleted(for group: TodoGroup) throws {
|
||||
// GIVEN
|
||||
let container = try modelContainer
|
||||
let context = container.mainContext
|
||||
@@ -69,7 +69,7 @@ struct TodoFetchDescriptorTests {
|
||||
}
|
||||
|
||||
@Test(
|
||||
"initializes with group and show completed enabled when filled tasks",
|
||||
"initialization with show completed enabled",
|
||||
arguments: TodoGroup.allCases
|
||||
)
|
||||
@MainActor
|
||||
@@ -90,7 +90,8 @@ struct TodoFetchDescriptorTests {
|
||||
// THEN
|
||||
if group == .overdue || group == .upcoming {
|
||||
#expect(results.count == 3)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
#expect(results.count == 2)
|
||||
}
|
||||
|
||||
@@ -99,16 +100,16 @@ struct TodoFetchDescriptorTests {
|
||||
#expect(results[0].title == "Completed task #2")
|
||||
#expect(results[1].title == "Completed task #1")
|
||||
case .overdue:
|
||||
#expect(results[0].title == "Completed task #1")
|
||||
#expect(results[1].title == "Overdue task #2")
|
||||
#expect(results[0].title == "Overdue task #2")
|
||||
#expect(results[1].title == "Completed task #1")
|
||||
#expect(results[2].title == "Overdue task #1")
|
||||
case .today:
|
||||
#expect(results[0].title == "Today task #1")
|
||||
#expect(results[1].title == "Today task #2")
|
||||
case .upcoming:
|
||||
#expect(results[0].title == "Upcoming task #1")
|
||||
#expect(results[1].title == "Upcoming task #2")
|
||||
#expect(results[2].title == "Completed task #2")
|
||||
#expect(results[0].title == "Completed task #2")
|
||||
#expect(results[1].title == "Upcoming task #1")
|
||||
#expect(results[2].title == "Upcoming task #2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ struct TodoFetchDescriptorTests {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension TodoFetchDescriptorTests {
|
||||
private extension TodoFetchDescriptor_InitsTests {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
@@ -138,38 +139,46 @@ private extension TodoFetchDescriptorTests {
|
||||
let tasks: [Todo] = [
|
||||
.init(
|
||||
title: "Today task #1",
|
||||
due: .now
|
||||
due: .now,
|
||||
order: 0
|
||||
),
|
||||
.init(
|
||||
title: "Today task #2",
|
||||
due: .now
|
||||
due: .now,
|
||||
order: 1
|
||||
),
|
||||
.init(
|
||||
title: "Upcoming task #1",
|
||||
due: calendar.date(byAdding: .day, value: 1, to: .now) ?? .now
|
||||
due: calendar.date(byAdding: .day, value: 2, to: .now) ?? .now,
|
||||
order: 0
|
||||
),
|
||||
.init(
|
||||
title: "Upcoming task #2",
|
||||
due: calendar.date(byAdding: .day, value: 1, to: .now) ?? .now
|
||||
due: calendar.date(byAdding: .day, value: 3, to: .now) ?? .now,
|
||||
order: 1
|
||||
),
|
||||
.init(
|
||||
title: "Overdue task #1",
|
||||
due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now
|
||||
due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now,
|
||||
order: 1
|
||||
),
|
||||
.init(
|
||||
title: "Overdue task #2",
|
||||
due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now
|
||||
due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now,
|
||||
order: 0
|
||||
),
|
||||
.init(
|
||||
title: "Completed task #1",
|
||||
due: calendar.date(byAdding: .day, value: -1, to: .now) ?? .now,
|
||||
completed: .now
|
||||
completed: .now,
|
||||
order: 1
|
||||
),
|
||||
.init(
|
||||
title: "Completed task #2",
|
||||
due: calendar.date(byAdding: .day, value: 1, to: .now) ?? .now,
|
||||
completed: .now
|
||||
)
|
||||
completed: .now,
|
||||
order: 0
|
||||
),
|
||||
]
|
||||
|
||||
for task in tasks {
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user