From c02f0276878528ad7cc806c761a395c8bd15d62e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 12 Feb 2026 04:08:30 +0100 Subject: [PATCH] Updated the ContentView view in the app target to support navigation split view and to show floating new task button on both sidebar and details views when required. --- .../View Models/ContentViewModel.swift | 39 +++++++++++++++ App/Sources/Views/ContentView.swift | 49 ++++++++++-------- App/Sources/Views/NewTaskView.swift | 3 -- App/Sources/Views/TaskListView.swift | 1 - .../View Models/ContentViewModelTests.swift | 50 ++++++++++++++++++- 5 files changed, 117 insertions(+), 25 deletions(-) diff --git a/App/Sources/View Models/ContentViewModel.swift b/App/Sources/View Models/ContentViewModel.swift index d565b92..915bba4 100644 --- a/App/Sources/View Models/ContentViewModel.swift +++ b/App/Sources/View Models/ContentViewModel.swift @@ -10,6 +10,7 @@ extension ContentView { /// /// The view model manages: /// - Presentation state for the new task view within a sheet. + /// - Selected task group for the navigation split view. @Observable final class ViewModel { @@ -18,22 +19,60 @@ extension ContentView { /// Whether the new task view should be shown. var newTaskShown: Bool + /// The currently selected todo task group in the navigation split view. + /// + /// When a user selects a group from the sidebar, this property is updated + /// to reflect the selection. The detail view displays content based on this value. + var group: TodoGroup? + // MARK: Initialization /// Initializes a new view model instance with default values. /// /// The view model is initialized with: /// - New task view hidden + /// - No task group selected init() { newTaskShown = false + group = nil + } + + // MARK: Computed + + /// Determines whether the new task button should be shown in the detail view. + /// + /// This prevents showing the add button when viewing completed tasks, + /// as users should not create new tasks in the completed state. + var showNewTaskButtonInDetails: Bool { + group != nil + && group != .completed } // MARK: Functions /// Shows the new task view by setting the presentation state to true. + /// + /// This method triggers the presentation of a sheet where users can create a new task. + /// Called when the floating action button is tapped. func showNewTask() { newTaskShown = true } + /// Determines whether the new task button should be shown in the sidebar. + /// + /// The button appears in the sidebar only on compact horizontal size class devices + /// (typically iPhones in portrait mode). This provides a way to create tasks when + /// the sidebar is displayed as a full screen view. + /// + /// - Parameter horizontalSizeClass: The current horizontal size class of the environment. + /// - Returns: `true` if the button should be shown in the sidebar, `false` otherwise. + func showNewTaskButtonInSidebar(when horizontalSizeClass: UserInterfaceSizeClass?) -> Bool { + guard let horizontalSizeClass else { + return false + } + + return horizontalSizeClass == .compact + } + } } diff --git a/App/Sources/Views/ContentView.swift b/App/Sources/Views/ContentView.swift index 0e861e8..9d8b4f0 100644 --- a/App/Sources/Views/ContentView.swift +++ b/App/Sources/Views/ContentView.swift @@ -4,13 +4,14 @@ import SwiftUI /// The main content view of the app. /// /// This view serves as the primary interface for managing todo tasks. -/// -/// The view uses a dedicated view model (`ViewModel`) to manage state and business logic, following the MVVM pattern. -/// User interactions are handled through the view model, maintaining separation of concerns. struct ContentView: View { // MARK: Properties + /// The horizontal size class of the environment. + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass + /// The view model managing the state and logic for this view. @State private var viewModel = ViewModel() @@ -19,30 +20,38 @@ struct ContentView: View { /// The content and behavior of the view. /// - /// Displays a navigation view containing: + /// Displays a navigation split view with: + /// - Sidebar: List of todo task groups + /// - Detail: List of todo task related to a selected todo task group /// - A floating action button for adding new tasks /// - /// The view presents a sheet when `viewModel.newTaskShown` is true, - /// allowing users to create new todo tasks. + /// The view presents a sheet to allow users to create new todo tasks. var body: some View { - NavigationView { - ZStack(alignment: .bottomTrailing) { - TaskGroupListView() - - Button(action: viewModel.showNewTask) { - Image(systemName: "plus") - .frame(width: 48, height: 48) - .foregroundStyle(.white) + NavigationSplitView { + TaskGroupListView($viewModel.group) + .newTaskButton(show: viewModel.showNewTaskButtonInSidebar(when: horizontalSizeClass)) { + viewModel.showNewTask() } - .glassEffect(.regular.tint(.blue.opacity(0.8)).interactive()) - .padding(.trailing, 20) + } detail: { + if let group = viewModel.group { + TaskListView(group: group) + .newTaskButton(show: viewModel.showNewTaskButtonInDetails) { + viewModel.showNewTask() + } + .sheet(isPresented: $viewModel.newTaskShown) { + NewTaskView() + } + } + else { + ContentUnavailableView( + "Select a Group", + systemImage: "sidebar.left", + description: Text("Choose a task group from the sidebar") + ) } - .navigationTitle("Todos") - } - .sheet(isPresented: $viewModel.newTaskShown) { - NewTaskView() } } + } // MARK: - Previews diff --git a/App/Sources/Views/NewTaskView.swift b/App/Sources/Views/NewTaskView.swift index b2a08e8..2979772 100644 --- a/App/Sources/Views/NewTaskView.swift +++ b/App/Sources/Views/NewTaskView.swift @@ -7,9 +7,6 @@ import SwiftUI /// - *Title* (required) /// - *Note* (optional) /// - *Due date* (required) -/// -/// The view uses a dedicated view model (`ViewModel`) to manage state and business logic, following the MVVM pattern. -/// When the user saves, the todo is inserted into the SwiftData model context and the view is dismissed afterwards. struct NewTaskView: View { // MARK: Properties diff --git a/App/Sources/Views/TaskListView.swift b/App/Sources/Views/TaskListView.swift index dacf3e2..deccbbb 100644 --- a/App/Sources/Views/TaskListView.swift +++ b/App/Sources/Views/TaskListView.swift @@ -4,7 +4,6 @@ import SwiftUI /// A view that displays filtered todo tasks for a specific task group. /// /// This view provides the presentation layer for displaying and managing tasks within a specific group category. -/// It follows the MVVM pattern, delegating business logic to a view model and data fetching to a separate content view. struct TaskListView: View { // MARK: Properties diff --git a/UnitTests/Tests/View Models/ContentViewModelTests.swift b/UnitTests/Tests/View Models/ContentViewModelTests.swift index 677520a..bb699b9 100644 --- a/UnitTests/Tests/View Models/ContentViewModelTests.swift +++ b/UnitTests/Tests/View Models/ContentViewModelTests.swift @@ -1,3 +1,4 @@ +import SwiftUI import Testing @testable import Todos @@ -16,11 +17,37 @@ struct ContentViewModelTests { // THEN #expect(viewModel.newTaskShown == false) + #expect(viewModel.group == nil) + } + + // MARK: - Computed Property Tests + + @Test( + "showNewTaskButtonInDetails returns expected value for every group", + arguments: zip( + [TodoGroup.today, .upcoming, .overdue, .completed, nil], + [true, true, true, false, false] + ) + ) + func showNewTaskButtonInDetails( + for group: TodoGroup?, + expects expectation: Bool + ) { + // GIVEN + let viewModel = ContentView.ViewModel() + + viewModel.group = group + + // WHEN + let shouldNewTaskButton = viewModel.showNewTaskButtonInDetails + + // THEN + #expect(shouldNewTaskButton == expectation) } // MARK: - Functions Tests - @Test("showNewTask sets showNewTaskView to true") + @Test("showNewTask sets newTaskShown to true") func newTaskShown() { // GIVEN let viewModel = ContentView.ViewModel() @@ -32,4 +59,25 @@ struct ContentViewModelTests { #expect(viewModel.newTaskShown == true) } + @Test( + "showNewTaskButtonInSidebar returns expected value for every horizontal size class", + arguments: zip( + [UserInterfaceSizeClass.compact, .regular, nil], + [true, false, false] + ) + ) + func showNewTaskButtonInSidebar( + for horizontalSizeClass: UserInterfaceSizeClass?, + expects expectation: Bool + ) { + // GIVEN + let viewModel = ContentView.ViewModel() + + // WHEN + let shouldNewTaskButton = viewModel.showNewTaskButtonInSidebar(when: horizontalSizeClass) + + // THEN + #expect(shouldNewTaskButton == expectation) + } + }