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.
This commit is contained in:
@@ -10,6 +10,7 @@ extension ContentView {
|
|||||||
///
|
///
|
||||||
/// The view model manages:
|
/// The view model manages:
|
||||||
/// - Presentation state for the new task view within a sheet.
|
/// - Presentation state for the new task view within a sheet.
|
||||||
|
/// - Selected task group for the navigation split view.
|
||||||
@Observable
|
@Observable
|
||||||
final class ViewModel {
|
final class ViewModel {
|
||||||
|
|
||||||
@@ -18,22 +19,60 @@ extension ContentView {
|
|||||||
/// Whether the new task view should be shown.
|
/// Whether the new task view should be shown.
|
||||||
var newTaskShown: Bool
|
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
|
// MARK: Initialization
|
||||||
|
|
||||||
/// Initializes a new view model instance with default values.
|
/// Initializes a new view model instance with default values.
|
||||||
///
|
///
|
||||||
/// The view model is initialized with:
|
/// The view model is initialized with:
|
||||||
/// - New task view hidden
|
/// - New task view hidden
|
||||||
|
/// - No task group selected
|
||||||
init() {
|
init() {
|
||||||
newTaskShown = false
|
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
|
// MARK: Functions
|
||||||
|
|
||||||
/// Shows the new task view by setting the presentation state to true.
|
/// 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() {
|
func showNewTask() {
|
||||||
newTaskShown = true
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import SwiftUI
|
|||||||
/// The main content view of the app.
|
/// The main content view of the app.
|
||||||
///
|
///
|
||||||
/// This view serves as the primary interface for managing todo tasks.
|
/// 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 {
|
struct ContentView: View {
|
||||||
|
|
||||||
// MARK: Properties
|
// 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.
|
/// The view model managing the state and logic for this view.
|
||||||
@State
|
@State
|
||||||
private var viewModel = ViewModel()
|
private var viewModel = ViewModel()
|
||||||
@@ -19,30 +20,38 @@ struct ContentView: View {
|
|||||||
|
|
||||||
/// The content and behavior of the 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
|
/// - A floating action button for adding new tasks
|
||||||
///
|
///
|
||||||
/// The view presents a sheet when `viewModel.newTaskShown` is true,
|
/// The view presents a sheet to allow users to create new todo tasks.
|
||||||
/// allowing users to create new todo tasks.
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationSplitView {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
TaskGroupListView($viewModel.group)
|
||||||
TaskGroupListView()
|
.newTaskButton(show: viewModel.showNewTaskButtonInSidebar(when: horizontalSizeClass)) {
|
||||||
|
viewModel.showNewTask()
|
||||||
Button(action: viewModel.showNewTask) {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
.frame(width: 48, height: 48)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
}
|
}
|
||||||
.glassEffect(.regular.tint(.blue.opacity(0.8)).interactive())
|
} detail: {
|
||||||
.padding(.trailing, 20)
|
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
|
// MARK: - Previews
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import SwiftUI
|
|||||||
/// - *Title* (required)
|
/// - *Title* (required)
|
||||||
/// - *Note* (optional)
|
/// - *Note* (optional)
|
||||||
/// - *Due date* (required)
|
/// - *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 {
|
struct NewTaskView: View {
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import SwiftUI
|
|||||||
/// A view that displays filtered todo tasks for a specific task group.
|
/// 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.
|
/// 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 {
|
struct TaskListView: View {
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import SwiftUI
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
@testable import Todos
|
@testable import Todos
|
||||||
@@ -16,11 +17,37 @@ struct ContentViewModelTests {
|
|||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
#expect(viewModel.newTaskShown == false)
|
#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
|
// MARK: - Functions Tests
|
||||||
|
|
||||||
@Test("showNewTask sets showNewTaskView to true")
|
@Test("showNewTask sets newTaskShown to true")
|
||||||
func newTaskShown() {
|
func newTaskShown() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
let viewModel = ContentView.ViewModel()
|
let viewModel = ContentView.ViewModel()
|
||||||
@@ -32,4 +59,25 @@ struct ContentViewModelTests {
|
|||||||
#expect(viewModel.newTaskShown == true)
|
#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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user