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:
@@ -0,0 +1,79 @@
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
/// A preview modifier that provides an empty SwiftData model container for SwiftUI previews.
|
||||
///
|
||||
/// This modifier creates an in-memory SwiftData model container with no data, useful for testing and previewing views in their empty or initial state.
|
||||
/// It's particularly helpful for testing empty state UI, loading states, or views that should gracefully handle no data.
|
||||
///
|
||||
/// ## Usage
|
||||
/// Use the `.emptyContainer` trait on your preview:
|
||||
///
|
||||
/// ```swift
|
||||
/// #Preview(traits: .emptyContainer) {
|
||||
/// TaskGroupListView()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This automatically provides an empty model container to the preview, allowing SwiftData queries to return empty results.
|
||||
struct EmptyContainerModifier: PreviewModifier {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Creates and configures a shared in-memory model container with no data.
|
||||
///
|
||||
/// This method is called once when the preview is loaded and creates an in-memory
|
||||
/// SwiftData container with no persisted entities. The container is configured
|
||||
/// to store data only in memory, ensuring previews don't affect any persistent storage.
|
||||
///
|
||||
/// - Returns: An empty `ModelContainer` configured for in-memory storage.
|
||||
/// - Throws: An error if the model container cannot be created.
|
||||
static func makeSharedContext() async throws -> ModelContainer {
|
||||
try ModelContainer(
|
||||
for: Todo.self,
|
||||
configurations: .init(isStoredInMemoryOnly: true)
|
||||
)
|
||||
}
|
||||
|
||||
/// Applies the empty model container to the preview content.
|
||||
///
|
||||
/// This method is called by SwiftUI to apply the modifier to the preview.
|
||||
/// It injects the empty model container into the SwiftUI environment, making it
|
||||
/// available to any views that use `@Query` or `@Environment(\.modelContext)`.
|
||||
/// Queries will return empty results, allowing you to test empty state behavior.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - content: The preview content to modify.
|
||||
/// - context: The empty model container.
|
||||
/// - Returns: The content with the empty model container applied.
|
||||
func body(
|
||||
content: Content,
|
||||
context: ModelContainer
|
||||
) -> some View {
|
||||
content.modelContainer(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - PreviewTrait
|
||||
|
||||
extension PreviewTrait where T == Preview.ViewTraits {
|
||||
|
||||
/// A preview trait that provides an empty model container.
|
||||
///
|
||||
/// Use this trait to test views in their empty state without any data.
|
||||
/// This is particularly useful for testing empty state UI, placeholder content,
|
||||
/// and ensuring views handle the absence of data gracefully.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```swift
|
||||
/// #Preview("Empty State", traits: .emptyContainer) {
|
||||
/// TaskGroupListView()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The view will receive an empty SwiftData container, causing any `@Query`
|
||||
/// properties to return empty collections.
|
||||
static var emptyContainer: Self = .modifier(EmptyContainerModifier())
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
/// A preview modifier that provides sample task data for SwiftUI previews.
|
||||
///
|
||||
/// This modifier creates an in-memory SwiftData model container populated with representative sample todos across all task groups
|
||||
/// (today, upcoming, overdue, completed).
|
||||
/// It's designed to be used with SwiftUI previews to display realistic data without
|
||||
/// requiring a persistent database.
|
||||
///
|
||||
/// ## Usage
|
||||
/// Use the `.sampleTasks` trait on your preview:
|
||||
///
|
||||
/// ```swift
|
||||
/// #Preview(traits: .sampleTasks) {
|
||||
/// TaskGroupListView()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This automatically provides a model container with sample data to the preview.
|
||||
struct SampleTaskDataModifier: PreviewModifier {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Creates and configures a shared in-memory model container with sample task data.
|
||||
///
|
||||
/// This method is called once when the preview is loaded and creates an in-memory
|
||||
/// SwiftData container populated with 8 representative todo tasks spanning all
|
||||
/// task groups and various configurations.
|
||||
///
|
||||
/// - Returns: A `ModelContainer` configured for in-memory storage with sample data.
|
||||
/// - Throws: An error if the model container cannot be created.
|
||||
static func makeSharedContext() async throws -> ModelContainer {
|
||||
let modelContainer = try ModelContainer(
|
||||
for: Todo.self,
|
||||
configurations: .init(isStoredInMemoryOnly: true)
|
||||
)
|
||||
|
||||
let calendar: Calendar = .current
|
||||
let today: Date = .now
|
||||
let tomorrow: Date = calendar.date(byAdding: .day, value: 1, to: today) ?? today
|
||||
let yesterday = calendar.date(byAdding: .day, value: -1, to: today) ?? today
|
||||
|
||||
let tasks: [Todo] = [
|
||||
.init(
|
||||
title: "Today task 1",
|
||||
due: today
|
||||
),
|
||||
.init(
|
||||
title: "Today task 2",
|
||||
note: "This is some random note attached to the task.",
|
||||
due: today
|
||||
),
|
||||
.init(
|
||||
title: "Tomorrow task",
|
||||
due: tomorrow
|
||||
),
|
||||
.init(
|
||||
title: "Next week task",
|
||||
note: "This is some random note attached to the task.",
|
||||
due: calendar.date(byAdding: .day, value: 7, to: today) ?? today
|
||||
),
|
||||
.init(
|
||||
title: "Yesterday task",
|
||||
due: yesterday
|
||||
),
|
||||
.init(
|
||||
title: "Last week task",
|
||||
note: "This is some random note attached to the task.",
|
||||
due: calendar.date(byAdding: .day, value: -7, to: today) ?? today
|
||||
),
|
||||
.init(
|
||||
title: "Completed task 1",
|
||||
due: today,
|
||||
completed: today
|
||||
),
|
||||
.init(
|
||||
title: "Completed task 2",
|
||||
note: "This is some random note attached to the task.",
|
||||
due: yesterday,
|
||||
completed: today
|
||||
),
|
||||
]
|
||||
|
||||
for task in tasks {
|
||||
modelContainer.mainContext.insert(task)
|
||||
}
|
||||
|
||||
return modelContainer
|
||||
}
|
||||
|
||||
/// Applies the model container to the preview content.
|
||||
///
|
||||
/// This method is called by SwiftUI to apply the modifier to the preview.
|
||||
/// It injects the model container into the SwiftUI environment, making it
|
||||
/// available to any views that use `@Query` or `@Environment(\.modelContext)`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - content: The preview content to modify.
|
||||
/// - context: The model container with sample data.
|
||||
/// - Returns: The content with the model container applied.
|
||||
func body(
|
||||
content: Content,
|
||||
context: ModelContainer
|
||||
) -> some View {
|
||||
content.modelContainer(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - PreviewTrait
|
||||
|
||||
extension PreviewTrait where T == Preview.ViewTraits {
|
||||
|
||||
/// A preview trait that provides sample task data.
|
||||
///
|
||||
/// Use this trait to automatically populate previews with realistic sample todos
|
||||
/// without manually creating a model container and inserting data.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```swift
|
||||
/// #Preview(traits: .sampleTasks) {
|
||||
/// TaskGroupListView()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This is equivalent to manually creating a container with sample data but
|
||||
/// much more concise and reusable across multiple previews.
|
||||
static var sampleTasks: Self = .modifier(SampleTaskDataModifier())
|
||||
|
||||
}
|
||||
@@ -1,6 +1,50 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"enumeration.todo-group.title.completed" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Completed"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"enumeration.todo-group.title.overdue" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Overdue"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"enumeration.todo-group.title.today" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Today"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"enumeration.todo-group.title.upcoming" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Upcoming"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"view.new-task.navigation.title" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -66,6 +110,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"view.task-group-list.navigation.title" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Todos"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.1"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Categorizes todo tasks into groups based on their due dates and completion status.
|
||||
///
|
||||
/// 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.
|
||||
enum TodoGroup {
|
||||
/// Tasks that have been marked as done, regardless of due date.
|
||||
case completed
|
||||
/// Tasks with due dates in the past that remain incomplete.
|
||||
case overdue
|
||||
/// Tasks due on the current day that are not yet completed.
|
||||
case today
|
||||
/// Tasks due in the future that are not yet completed.
|
||||
case upcoming
|
||||
}
|
||||
|
||||
// MARK: - Computed
|
||||
|
||||
extension TodoGroup {
|
||||
|
||||
/// The color associated with the task group.
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .completed: .gray
|
||||
case .overdue: .red
|
||||
case .today: .blue
|
||||
case .upcoming: .green
|
||||
}
|
||||
}
|
||||
|
||||
/// The system icon name for the task group.
|
||||
var iconName: String {
|
||||
switch self {
|
||||
case .completed: "checkmark.circle.fill"
|
||||
case .overdue: "calendar.badge.exclamationmark"
|
||||
case .today: "calendar.badge.clock"
|
||||
case .upcoming: "calendar"
|
||||
}
|
||||
}
|
||||
|
||||
/// The display title for the task group.
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .completed: "enumeration.todo-group.title.completed"
|
||||
case .overdue: "enumeration.todo-group.title.overdue"
|
||||
case .today: "enumeration.todo-group.title.today"
|
||||
case .upcoming: "enumeration.todo-group.title.upcoming"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - CaseIterable
|
||||
|
||||
extension TodoGroup: CaseIterable {
|
||||
|
||||
static var allCases: [TodoGroup] { [.today, .upcoming, .overdue, .completed] }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Identifiable
|
||||
|
||||
extension TodoGroup: Identifiable {
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
}
|
||||
@@ -11,7 +11,7 @@ extension ContentView {
|
||||
/// The view model manages:
|
||||
/// - Presentation state for the new task view within a sheet.
|
||||
@Observable
|
||||
class ViewModel {
|
||||
final class ViewModel {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ extension NewTaskView {
|
||||
/// - Form validation (ensuring title is not empty)
|
||||
/// - Saving the todo to SwiftData model context
|
||||
@Observable
|
||||
class ViewModel {
|
||||
final class ViewModel {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
|
||||
extension TaskGroupListView {
|
||||
|
||||
/// View model for managing the logic for the task group list view.
|
||||
///
|
||||
/// This observable class encapsulates the state and business logic for the task group list view.
|
||||
/// It follows the MVVM pattern to separate presentation logic from the view layer.
|
||||
///
|
||||
/// ## Responsibilities
|
||||
/// - Providing access to all task groups for iteration
|
||||
/// - Efficiently counting tasks per group using SwiftData fetch descriptors
|
||||
/// - Managing date calculations for group categorization
|
||||
@Observable
|
||||
final class ViewModel {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The calendar used for date calculations.
|
||||
///
|
||||
/// This calendar is used to determine day boundaries for categorizing tasks into date groups: today, upcoming, overdue.
|
||||
/// Defaults to the current calendar.
|
||||
private let calendar: Calendar
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initializes a new view model instance.
|
||||
///
|
||||
/// - Parameter calendar: The calendar to use for date calculations. Defaults to `.current`.
|
||||
init(calendar: Calendar = .current) {
|
||||
self.calendar = calendar
|
||||
}
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// Returns all available task groups in an predetermined order.
|
||||
///
|
||||
/// This computed property provides access to all task group cases for iteration in the view.
|
||||
///
|
||||
/// - Returns: An array of all `TodoGroup` cases in a predetermined order.
|
||||
var groups: [TodoGroup] {
|
||||
TodoGroup.allCases
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Calculates the number of tasks for a given task group from an in-memory collection.
|
||||
///
|
||||
/// 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.
|
||||
/// - Returns: The number of tasks in the specified group.
|
||||
func count(
|
||||
_ tasks: [Todo],
|
||||
for group: TodoGroup
|
||||
) -> Int {
|
||||
let today = calendar.startOfDay(for: .now)
|
||||
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today) ?? .now
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
.count
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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