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 // 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. /// This method filters the provided tasks array to count only those tasks that belong
/// Tasks are categorized based on their due date and completion status. /// 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: /// - Parameters:
/// - tasks: The array of tasks to filter and count.
/// - group: The task group to count tasks for. /// - group: The task group to count tasks for.
/// - modelContext: The SwiftData model context to query. /// - Returns: The number of tasks in the specified group.
/// - Returns: The number of tasks in the specified group, or 0 if an error occurs.
func count( func count(
for group: TodoGroup, _ tasks: [Todo],
in modelContext: ModelContext for group: TodoGroup
) -> Int { ) -> Int {
let today = calendar.startOfDay(for: .now) let today = calendar.startOfDay(for: .now)
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? .now let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? .now
let fetchDescriptor: FetchDescriptor<Todo> = return tasks
.filter { task in
switch group { switch group {
case .completed: case .completed:
.init(predicate: #Predicate { $0.completed != nil }) task.completed != nil
case .overdue: case .overdue:
.init(predicate: #Predicate { $0.completed == nil && $0.due < today }) task.completed == nil && task.due < today
case .today: case .today:
.init(predicate: #Predicate { $0.completed == nil && $0.due >= today && $0.due < tomorrow }) task.completed == nil && task.due >= today && task.due < tomorrow
case .upcoming: case .upcoming:
.init(predicate: #Predicate { $0.completed == nil && $0.due >= tomorrow }) 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 // 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. /// This query triggers view updates whenever todo tasks are inserted, updated, or deleted,
@Environment(\.modelContext) /// ensuring the task counts displayed in the view remain accurate.
private var modelContext @Query
private var tasks: [Todo]
/// The view model managing the state and logic for this view. /// The view model managing the state and logic for this view.
/// ///
@@ -43,10 +44,7 @@ struct TaskGroupListView: View {
NavigationLink(destination: TaskListView(group: group)) { NavigationLink(destination: TaskListView(group: group)) {
Card( Card(
group: group, group: group,
count: viewModel.count( count: viewModel.count(tasks, for: group)
for: group,
in: modelContext
)
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -31,58 +31,49 @@ struct TaskGroupListViewModelTests {
// MARK: Functions tests // MARK: Functions tests
@MainActor
@Test( @Test(
"count returns zero when empty data", "count returns zero when empty data",
arguments: TodoGroup.allCases arguments: TodoGroup.allCases
) )
func countWhenEmptyData(for group: TodoGroup) throws { func countWhenEmptyData(for group: TodoGroup) throws {
// GIVEN // GIVEN
let container = try ModelContainer( let tasks: [Todo] = []
for: Todo.self,
configurations: .init(isStoredInMemoryOnly: true)
)
let viewModel = TaskGroupListView.ViewModel() let viewModel = TaskGroupListView.ViewModel()
// WHEN // WHEN
let count = viewModel.count(for: group, in: container.mainContext) let count = viewModel.count(tasks, for: group)
// THEN // THEN
#expect(count == 0) #expect(count == 0)
} }
@MainActor
@Test( @Test(
"count returns correct count when filled data", "count returns correct count when filled data",
arguments: TodoGroup.allCases arguments: TodoGroup.allCases
) )
func countWhenFilledData(for group: TodoGroup) throws { func countWhenFilledData(for group: TodoGroup) throws {
// GIVEN // GIVEN
let container = try ModelContainer(
for: Todo.self,
configurations: .init(isStoredInMemoryOnly: true)
)
let context = container.mainContext
let today: Date = .now let today: Date = .now
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? today let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? today
let yesterday = 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 nextWeek = calendar.date(byAdding: .day, value: 7, to: today) ?? today
let lastWeek = 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)) let tasks = [
context.insert(Todo(title: "Today task #2", due: today)) Todo(title: "Today task #1", due: today),
context.insert(Todo(title: "Tomorrow task #1", due: tomorrow)) Todo(title: "Today task #2", due: today),
context.insert(Todo(title: "Next week task #1", due: nextWeek)) Todo(title: "Tomorrow task #1", due: tomorrow),
context.insert(Todo(title: "Yesterday task #1", due: yesterday)) Todo(title: "Next week task #1", due: nextWeek),
context.insert(Todo(title: "Last week task #1", due: lastWeek)) Todo(title: "Yesterday task #1", due: yesterday),
context.insert(Todo(title: "Completed task #1", due: tomorrow, completed: today)) Todo(title: "Last week task #1", due: lastWeek),
context.insert(Todo(title: "Completed task #2", due: yesterday, completed: today)) Todo(title: "Completed task #1", due: tomorrow, completed: today),
Todo(title: "Completed task #2", due: yesterday, completed: today)
]
let viewModel = TaskGroupListView.ViewModel() let viewModel = TaskGroupListView.ViewModel()
// WHEN // WHEN
let count = viewModel.count(for: group, in: context) let count = viewModel.count(tasks, for: group)
// THEN // THEN
#expect(count == 2) #expect(count == 2)