App layout improvements #10
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
.navigationTitle("Todos")
|
||||
} 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")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user