diff --git a/App/Resources/Localizable.xcstrings b/App/Resources/Localizable.xcstrings index ca9900d..068d18e 100644 --- a/App/Resources/Localizable.xcstrings +++ b/App/Resources/Localizable.xcstrings @@ -154,6 +154,16 @@ } } }, + "view.task-list.content-unavailable.title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All Tasks Completed" + } + } + } } }, "version" : "1.1" diff --git a/App/Sources/View Models/TaskListViewModel.swift b/App/Sources/View Models/TaskListViewModel.swift new file mode 100644 index 0000000..f2ea825 --- /dev/null +++ b/App/Sources/View Models/TaskListViewModel.swift @@ -0,0 +1,43 @@ +import SwiftData +import SwiftUI + +extension TaskListView { + + /// View model for managing the logic for the task list view. + /// + /// This observable class encapsulates the state and business logic for the task list view. + /// It follows the MVVM pattern to separate presentation logic from the view layer. + /// + /// ## Responsibilities + /// - Providing the navigation title for the current group + @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. + private let group: TodoGroup + + // MARK: Initialization + + /// Initializes a new view model instance for a specific task group. + /// + /// - Parameter group: The task group to display tasks for. + init(group: TodoGroup) { + self.group = group + } + + // MARK: Computed + + /// The navigation title for the current task group. + /// + /// This computed property provides the localized title for the current group, + /// suitable for display in a navigation bar. + /// + /// - Returns: The localized title key for the current group. + var navigationTitle: LocalizedStringKey { group.title } + + } +} diff --git a/App/Sources/Views/TaskListView.swift b/App/Sources/Views/TaskListView.swift new file mode 100644 index 0000000..78cd378 --- /dev/null +++ b/App/Sources/Views/TaskListView.swift @@ -0,0 +1,99 @@ +import SwiftData +import SwiftUI + +/// A view that displays all todo tasks for a specific task group. +/// +/// This view shows a filtered list of tasks based on the selected group (today, upcoming, past, or completed). +/// The view uses a view model to separate business logic from presentation. +struct TaskListView: View { + + // MARK: Properties + + /// Query to fetch todos filtered by the current group. + @Query + private var tasks: [Todo] + + /// The view model managing the business logic for this view. + @State + private var viewModel: ViewModel + + // MARK: Initialization + + /// Creates a new task list view for a specific group. + /// - Parameter group: The todo group to display tasks for. + init(group: TodoGroup) { + _tasks = .init(FetchDescriptor(group: group)) + _viewModel = .init(initialValue: ViewModel(group: group)) + } + + // MARK: Body + + /// The content and behavior of the view. + /// + /// Displays a list of tasks filtered by the current group. + var body: some View { + Group { + if tasks.isEmpty { + ContentUnavailableView( + "view.task-list.content-unavailable.title", + systemImage: "checkmark.circle" + ) + } else { + List { + ForEach(tasks) { todo in + Item(todo) + } + } + .listStyle(.grouped) + } + } + .navigationTitle(viewModel.navigationTitle) + } +} + +// MARK: - Previews + +#Preview( + "Task list with empty tasks", + traits: .emptyContainer +) { + NavigationStack { + TaskListView(group: .today) + } +} + +#Preview( + "Task list - today", + traits: .sampleTasks +) { + NavigationStack { + TaskListView(group: .today) + } +} + +#Preview( + "Task list - upcoming", + traits: .sampleTasks +) { + NavigationStack { + TaskListView(group: .upcoming) + } +} + +#Preview( + "Task list - overdue", + traits: .sampleTasks +) { + NavigationStack { + TaskListView(group: .overdue) + } +} + +#Preview( + "Task list - completed", + traits: .sampleTasks +) { + NavigationStack { + TaskListView(group: .completed) + } +} diff --git a/UnitTests/Tests/View Models/TaskListViewModelTests.swift b/UnitTests/Tests/View Models/TaskListViewModelTests.swift new file mode 100644 index 0000000..7693472 --- /dev/null +++ b/UnitTests/Tests/View Models/TaskListViewModelTests.swift @@ -0,0 +1,27 @@ +import Foundation +import SwiftData +import Testing + +@testable import Todos + +@Suite("TaskList view model tests") +struct TaskListViewModelTests { + + // MARK: Computed tests + + @Test( + "navigationTitle returns correct title for each and every group", + arguments: TodoGroup.allCases + ) + func navigationTitle(for group: TodoGroup) { + // GIVEN + let viewModel = TaskListView.ViewModel(group: group) + + // WHEN + let title = viewModel.navigationTitle + + // THEN + #expect(title == group.title) + } + +}