Files
todos/App/Sources/Views/TaskList/TaskListItem.swift
T

199 lines
6.3 KiB
Swift

import SwiftUI
extension TaskListView {
/// A view that displays a single editable todo task in a list.
///
/// This view provides a comprehensive interface for viewing and editing task information,
/// including:
/// - A completion toggle using a circular checkbox design
/// - An editable title field with multi-line support
/// - An optional note field that appears when the task has notes
/// - A date picker for selecting the task's due date
///
/// The view automatically adjusts its appearance based on completion state, dimming
/// completed tasks to provide visual feedback. All text fields support automatic
/// capitalization and keyboard correction for improved user experience.
struct Item: View {
// MARK: Properties
/// Tracks which text field currently has keyboard focus.
@FocusState
private var inputFocus: InputFieldFocus?
/// The todo task to display.
@Bindable
private var item: Todo
// MARK: Initializers
/// Creates a new task list item view for the specified todo.
///
/// The item is wrapped in a `@Bindable` property wrapper to enable two-way
/// data binding for editing task properties directly within the view.
///
/// - Parameter item: The todo task to display and edit.
init(_ item: Todo) {
self._item = Bindable(item)
}
// MARK: Body
/// The content and behavior of the view.
///
/// Renders an editable task list item with a completion toggle, title field,
/// optional note field, and date picker. The view's foreground color automatically
/// adjusts based on completion state, and changes animate smoothly.
var body: some View {
HStack(
alignment: .top,
spacing: 16
) {
Toggle(
"view.task-list-item.placeholder.completed",
isOn: isCompleted
)
.sensoryFeedback(trigger: item.completed) {
item.completed != nil ? .increase : .decrease
}
.toggleStyle(.completion)
VStack(
alignment: .leading,
spacing: 8
) {
TextField(
"view.task-list-item.placeholder.title",
text: $item.title,
axis: .vertical
)
.font(.headline)
.focused(
$inputFocus,
equals: .title
)
.onNewlineFocus(
$item.title,
to: .note,
on: $inputFocus
)
if isNoteShown {
TextField(
"view.task-list-item.placeholder.note",
text: note,
axis: .vertical
)
.font(.subheadline)
.focused(
$inputFocus,
equals: .note
)
.onEmptyNewlineResignFocus(
text: note,
from: $inputFocus
)
}
DatePicker(
selection: $item.due,
displayedComponents: .date
) {
Image(systemName: "calendar")
}
.frame(width: 160)
.disabled(isCompleted.wrappedValue)
}
.autocapitalization(.sentences)
.autocorrectionDisabled(false)
.foregroundStyle(foregroundStyleView)
.keyboardType(.default)
}
.animation(
.easeInOut,
value: item.completed
)
}
}
}
// MARK: - Helpers
private extension TaskListView.Item {
// MARK: Bindings
/// A binding to the task's completion state.
///
/// This computed binding converts between the optional `completed` date and a boolean
/// value for the completion toggle. When set to `true`, it marks the task as completed
/// with the current date; when set to `false`, it clears the completion date.
var isCompleted: Binding<Bool> {
.init {
item.completed != nil
} set: {
item.completed = $0 ? .now : nil
}
}
/// A binding to the note text of a todo task.
///
/// This computed binding provides a non-optional string binding for the note field,
/// converting `nil` values to empty strings for seamless text field interaction.
var note: Binding<String> {
.init {
item.note ?? ""
} set: {
item.note = $0.isEmpty ? nil : $0
}
}
// MARK: Computed
/// The foreground color style for the components in this view.
///
/// Returns `.primary` for incomplete tasks and `.secondary` for completed tasks,
/// providing visual feedback about the task's completion state by dimming
/// completed items.
var foregroundStyleView: Color {
item.completed == nil ? .primary : .secondary
}
/// Determines whether the note field should be visible.
///
/// The note field is shown when:
/// - Any text field has focus (allowing users to add a note even if one doesn't exist yet)
/// - The todo task has a note with non-whitespace content
var isNoteShown: Bool {
guard inputFocus != nil else {
guard let note = item.note else {
return false
}
return note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
}
return true
}
}
// MARK: - Previews
#Preview("Task list item") {
@Previewable
var todo: Todo = .init(
title: "Complete project documentation",
note: "Add detailed comments and README",
due: .now,
completed: nil
)
List {
TaskListView.Item(todo)
}
.listStyle(.grouped)
}