Group tasks feature (#3)
This PR contains the work done to implement the **Group Task** feature, and to integrate it into the main content view of the app. Furthermore, an issue related to triggering a view update after inserting the todo tasks into the model container has been fixed. Reviewed-on: rock-n-code/todos-app#3 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #3.
This commit is contained in:
@@ -27,7 +27,7 @@ struct ContentView: View {
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
List {}
|
||||
TaskGroupListView()
|
||||
|
||||
Button(action: viewModel.showNewTask) {
|
||||
Image(systemName: "plus")
|
||||
|
||||
@@ -52,7 +52,7 @@ struct NewTaskView: View {
|
||||
"view.new-task.placeholder.title",
|
||||
text: $viewModel.title
|
||||
)
|
||||
.autocapitalization(.none)
|
||||
.autocapitalization(.sentences)
|
||||
.autocorrectionDisabled(false)
|
||||
.keyboardType(.asciiCapable)
|
||||
|
||||
@@ -61,7 +61,7 @@ struct NewTaskView: View {
|
||||
text: $viewModel.note,
|
||||
axis: .vertical
|
||||
)
|
||||
.autocapitalization(.none)
|
||||
.autocapitalization(.sentences)
|
||||
.autocorrectionDisabled(false)
|
||||
.keyboardType(.asciiCapable)
|
||||
.lineLimit(3 ... 6)
|
||||
@@ -99,7 +99,9 @@ struct NewTaskView: View {
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("New todo task") {
|
||||
#Preview(
|
||||
"New todo task",
|
||||
traits: .emptyContainer
|
||||
) {
|
||||
NewTaskView()
|
||||
.modelContainer(for: Todo.self, inMemory: true)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import SwiftUI
|
||||
|
||||
extension TaskGroupListView {
|
||||
|
||||
/// A card-style view representing a single todo task group with its count.
|
||||
///
|
||||
/// This view displays a task group as a compact card showing:
|
||||
/// - The group's icon and title on the left
|
||||
/// - The task count on the right
|
||||
/// - A tinted background using the group's color
|
||||
///
|
||||
/// The view is designed to be used within the `TaskGroupListView` and is typically
|
||||
/// wrapped in a `NavigationLink` to allow navigation to detailed task lists.
|
||||
struct Card: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The number of tasks.
|
||||
private let count: Int
|
||||
|
||||
/// The task group.
|
||||
private let group: TodoGroup
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a task group list card.
|
||||
/// - Parameters:
|
||||
/// - group: The task group to display.
|
||||
/// - count: The number of tasks in the group.
|
||||
init(
|
||||
group: TodoGroup,
|
||||
count: Int
|
||||
) {
|
||||
self.count = count
|
||||
self.group = group
|
||||
}
|
||||
|
||||
// MARK: Body
|
||||
|
||||
/// The content and behavior of the view.
|
||||
///
|
||||
/// Displays a horizontal card layout with the group icon and title on the left,
|
||||
/// and the task count on the right, all within a colored background.
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(
|
||||
alignment: .leading,
|
||||
spacing: 8
|
||||
) {
|
||||
Image(systemName: group.iconName)
|
||||
.font(.system(size: 40))
|
||||
.foregroundStyle(group.color)
|
||||
|
||||
Text(group.title)
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
alignment: .leading
|
||||
)
|
||||
}
|
||||
.padding(.vertical, 24)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(count)")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(group.color)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(group.color.opacity(0.1))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Today item") {
|
||||
TaskGroupListView.Card(
|
||||
group: .today,
|
||||
count: .random(in: 0...20)
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
#Preview("Upcoming item") {
|
||||
TaskGroupListView.Card(
|
||||
group: .upcoming,
|
||||
count: .random(in: 0...20)
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
#Preview("Overdue item") {
|
||||
TaskGroupListView.Card(
|
||||
group: .overdue,
|
||||
count: .random(in: 0...20)
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
#Preview("Completed item") {
|
||||
TaskGroupListView.Card(
|
||||
group: .completed,
|
||||
count: .random(in: 0...20)
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
/// A view that displays todo tasks grouped by date and completion categories.
|
||||
///
|
||||
/// This view serves as the main interface for browsing tasks organized into four groups:
|
||||
/// - **Today**: Tasks due today that are not yet completed
|
||||
/// - **Upcoming**: Tasks due in the future that are not yet completed
|
||||
/// - **Overdue**: Tasks due in the past that are not yet completed
|
||||
/// - **Completed**: Tasks that have been marked as done
|
||||
struct TaskGroupListView: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Query to monitor all todo tasks for changes.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// Handles task counting and provides access to all task groups.
|
||||
/// Initialized with the default calendar for date calculations.
|
||||
@State
|
||||
private var viewModel = ViewModel()
|
||||
|
||||
// MARK: Body
|
||||
|
||||
/// The content and behavior of the view.
|
||||
///
|
||||
/// Displays a scrollable vertical list of task group cards. Each card shows:
|
||||
/// - The group's icon in its associated color
|
||||
/// - The group's title
|
||||
/// - The current count of tasks in that group
|
||||
///
|
||||
/// Cards are tappable and navigate to a detailed task list for the selected group.
|
||||
/// Task counts are fetched on-demand from the model context for each render.
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(viewModel.groups) { group in
|
||||
NavigationLink(destination: TaskListView(group: group)) {
|
||||
Card(
|
||||
group: group,
|
||||
count: viewModel.count(tasks, for: group)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.navigationTitle("view.task-group-list.navigation.title")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview(
|
||||
"Task group list with empty tasks",
|
||||
traits: .emptyContainer
|
||||
) {
|
||||
NavigationStack {
|
||||
TaskGroupListView()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(
|
||||
"Task group list with sample tasks",
|
||||
traits: .sampleTasks
|
||||
) {
|
||||
NavigationStack {
|
||||
TaskGroupListView()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user