Group tasks feature #3

Merged
javier merged 7 commits from app/group-tasks into main 2026-02-10 02:22:41 +00:00
3 changed files with 45 additions and 55 deletions
Showing only changes of commit 937941f50b - Show all commits
@@ -45,41 +45,42 @@ extension TaskGroupListView {
// MARK: Functions
/// Calculates the number of tasks for a given task group using SwiftData.
/// Calculates the number of tasks for a given task group from an in-memory collection.
///
/// This method performs an efficient database-level count using SwiftData's `fetchCount` method with predicate-based filtering.
/// Tasks are categorized based on their due date and completion status.
/// This method filters the provided tasks array to count only those tasks that belong
/// to the specified group based on their due date and completion status. Tasks are
/// categorized as follows:
///
/// - **Completed**: Tasks with a non-nil `completed` date
/// - **Overdue**: Incomplete tasks due before today
/// - **Today**: Incomplete tasks due today (from start of day to end of day)
/// - **Upcoming**: Incomplete tasks due tomorrow or later
///
/// - Parameters:
/// - tasks: The array of tasks to filter and count.
/// - group: The task group to count tasks for.
/// - modelContext: The SwiftData model context to query.
/// - Returns: The number of tasks in the specified group, or 0 if an error occurs.
/// - Returns: The number of tasks in the specified group.
func count(
for group: TodoGroup,
in modelContext: ModelContext
_ tasks: [Todo],
for group: TodoGroup
) -> Int {
let today = calendar.startOfDay(for: .now)
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? .now
let fetchDescriptor: FetchDescriptor<Todo> =
switch group {
case .completed:
.init(predicate: #Predicate { $0.completed != nil })
case .overdue:
.init(predicate: #Predicate { $0.completed == nil && $0.due < today })
case .today:
.init(predicate: #Predicate { $0.completed == nil && $0.due >= today && $0.due < tomorrow })
case .upcoming:
.init(predicate: #Predicate { $0.completed == nil && $0.due >= tomorrow })
return tasks
.filter { task in
switch group {
case .completed:
task.completed != nil
case .overdue:
task.completed == nil && task.due < today
case .today:
task.completed == nil && task.due >= today && task.due < tomorrow
case .upcoming:
task.completed == nil && task.due >= tomorrow
}
}
do {
return try modelContext.fetchCount(fetchDescriptor)
}
catch {
print("Error fetching count for group \(group): \(error)")
return 0
}
.count
}
}
+6 -8
View File
@@ -12,11 +12,12 @@ struct TaskGroupListView: View {
// MARK: Properties
/// The SwiftData model context for database operations.
/// Query to monitor all todo tasks for changes.
///
/// This context is used by the view model to perform efficient database-level queries for counting tasks in each group.
@Environment(\.modelContext)
private var modelContext
/// This query triggers view updates whenever todo tasks are inserted, updated, or deleted,
/// ensuring the task counts displayed in the view remain accurate.
@Query
private var tasks: [Todo]
/// The view model managing the state and logic for this view.
///
@@ -43,10 +44,7 @@ struct TaskGroupListView: View {
NavigationLink(destination: TaskListView(group: group)) {
Card(
group: group,
count: viewModel.count(
for: group,
in: modelContext
)
count: viewModel.count(tasks, for: group)
)
}
.buttonStyle(.plain)
@@ -31,58 +31,49 @@ struct TaskGroupListViewModelTests {
// MARK: Functions tests
@MainActor
@Test(
"count returns zero when empty data",
arguments: TodoGroup.allCases
)
func countWhenEmptyData(for group: TodoGroup) throws {
// GIVEN
let container = try ModelContainer(
for: Todo.self,
configurations: .init(isStoredInMemoryOnly: true)
)
let tasks: [Todo] = []
let viewModel = TaskGroupListView.ViewModel()
// WHEN
let count = viewModel.count(for: group, in: container.mainContext)
let count = viewModel.count(tasks, for: group)
// THEN
#expect(count == 0)
}
@MainActor
@Test(
"count returns correct count when filled data",
arguments: TodoGroup.allCases
)
func countWhenFilledData(for group: TodoGroup) throws {
// GIVEN
let container = try ModelContainer(
for: Todo.self,
configurations: .init(isStoredInMemoryOnly: true)
)
let context = container.mainContext
let today: Date = .now
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? today
let yesterday = calendar.date(byAdding: .day, value: -1, to: today) ?? today
let nextWeek = calendar.date(byAdding: .day, value: 7, to: today) ?? today
let lastWeek = calendar.date(byAdding: .day, value: -7, to: today) ?? today
context.insert(Todo(title: "Today task #1", due: today))
context.insert(Todo(title: "Today task #2", due: today))
context.insert(Todo(title: "Tomorrow task #1", due: tomorrow))
context.insert(Todo(title: "Next week task #1", due: nextWeek))
context.insert(Todo(title: "Yesterday task #1", due: yesterday))
context.insert(Todo(title: "Last week task #1", due: lastWeek))
context.insert(Todo(title: "Completed task #1", due: tomorrow, completed: today))
context.insert(Todo(title: "Completed task #2", due: yesterday, completed: today))
let tasks = [
Todo(title: "Today task #1", due: today),
Todo(title: "Today task #2", due: today),
Todo(title: "Tomorrow task #1", due: tomorrow),
Todo(title: "Next week task #1", due: nextWeek),
Todo(title: "Yesterday task #1", due: yesterday),
Todo(title: "Last week task #1", due: lastWeek),
Todo(title: "Completed task #1", due: tomorrow, completed: today),
Todo(title: "Completed task #2", due: yesterday, completed: today)
]
let viewModel = TaskGroupListView.ViewModel()
// WHEN
let count = viewModel.count(for: group, in: context)
let count = viewModel.count(tasks, for: group)
// THEN
#expect(count == 2)