273 lines
8.4 KiB
Swift
273 lines
8.4 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 focusOnField: FieldFocus?
|
|
|
|
/// 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) {
|
|
_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: title,
|
|
axis: .vertical
|
|
)
|
|
.font(.headline)
|
|
.focused(
|
|
$focusOnField,
|
|
equals: .title
|
|
)
|
|
|
|
if isNoteShown {
|
|
TextField(
|
|
"view.task-list-item.placeholder.note",
|
|
text: note,
|
|
axis: .vertical
|
|
)
|
|
.font(.subheadline)
|
|
.focused(
|
|
$focusOnField,
|
|
equals: .note
|
|
)
|
|
}
|
|
|
|
DatePicker(
|
|
selection: $item.due,
|
|
displayedComponents: .date
|
|
) {
|
|
Image(systemName: "calendar")
|
|
}
|
|
.frame(width: 160)
|
|
.disabled(isCompleted.wrappedValue)
|
|
}
|
|
.autocapitalization(.sentences)
|
|
.autocorrectionDisabled(false)
|
|
.foregroundStyle(foregroundStyleView)
|
|
.keyboardType(.asciiCapable)
|
|
}
|
|
.animation(
|
|
.easeInOut,
|
|
value: item.completed
|
|
)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Enumerations
|
|
|
|
private extension TaskListView.Item {
|
|
|
|
/// Identifies which text field should receive keyboard focus.
|
|
enum FieldFocus {
|
|
/// The note text field has focus.
|
|
case note
|
|
/// The title text field has focus.
|
|
case title
|
|
}
|
|
|
|
}
|
|
|
|
// 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: {
|
|
setNote(with: $0)
|
|
}
|
|
}
|
|
|
|
/// A binding to the title text of a todo task.
|
|
///
|
|
/// This computed binding provides access to the title of a todo task with special handling
|
|
/// for newline characters.
|
|
var title: Binding<String> {
|
|
.init {
|
|
item.title
|
|
} set: {
|
|
setTitle(with: $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 focusOnField != nil else {
|
|
guard let note = item.note else {
|
|
return false
|
|
}
|
|
|
|
return note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// MARK: Functions
|
|
|
|
/// Updates the note of a todo task with special handling for newlines and empty content.
|
|
///
|
|
/// This method provides intelligent note field behavior:
|
|
/// - Empty text immediately clears the note
|
|
/// - Newlines at the end of the text trigger special handling:
|
|
/// - If the text is only whitespace/newlines, the note is cleared and focus is removed
|
|
/// - Otherwise, the text (including newlines) is saved normally
|
|
/// - All other text is saved as-is
|
|
///
|
|
/// This allows users to press return on an empty note field to dismiss it,
|
|
/// while still supporting multi-line notes.
|
|
///
|
|
/// - Parameter text: The new note text to set.
|
|
func setNote(with text: String) {
|
|
guard text.isEmpty == false else {
|
|
item.note = text
|
|
return
|
|
}
|
|
|
|
if text.last == .newLine {
|
|
guard text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true else {
|
|
item.note = text
|
|
return
|
|
}
|
|
|
|
item.note = nil
|
|
focusOnField = nil
|
|
}
|
|
else {
|
|
item.note = text
|
|
}
|
|
}
|
|
|
|
/// Updates the title of a todo task with special handling for newline characters.
|
|
///
|
|
/// When the user presses return in the title field (adding a newline), this function
|
|
/// prevents the newline from being added to the title and instead moves keyboard
|
|
/// focus to the note field. If no newline is present, the title is updated normally.
|
|
///
|
|
/// - Parameter text: The new title text to set.
|
|
func setTitle(with text: String) {
|
|
guard text.lastIndex(of: .newLine) == nil else {
|
|
focusOnField = .note
|
|
return
|
|
}
|
|
|
|
item.title = text
|
|
}
|
|
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// MARK: - Character+Constants
|
|
|
|
private extension Character {
|
|
static let newLine: Self = "\n"
|
|
}
|