// // TodosView.swift // iris // // Created by Codex. // import SwiftUI struct TodosView: View { @StateObject private var model = TodosViewModel() @State private var isDoneExpanded = false @State private var lastDoneCount = 0 private var openTodos: [TodoItem] { model.todos.filter { !$0.isCompleted } } private var doneTodos: [TodoItem] { model.todos.filter { $0.isCompleted } } var body: some View { NavigationStack { List { Section("Add") { HStack(spacing: 12) { TextField("Add a todo", text: $model.newTitle) .textInputAutocapitalization(.sentences) .disableAutocorrection(false) .submitLabel(.done) .onSubmit { model.addTodo() } Button { model.addTodo() } label: { Image(systemName: "plus.circle.fill") .font(.title3) } .buttonStyle(.plain) .disabled(model.newTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) .accessibilityLabel("Add todo") } } Section("Todos") { if openTodos.isEmpty { Text("No open todos") .foregroundStyle(.secondary) } else { ForEach(openTodos) { item in TodoRow( item: item, onToggle: { model.toggleCompleted(id: item.id) }, onDelete: { model.deleteTodo(id: item.id) }, onUpdateTitle: { model.updateTitle(id: item.id, title: $0) } ) } } } Section { if isDoneExpanded { if doneTodos.isEmpty { Text("No completed todos") .foregroundStyle(.secondary) } else { ForEach(doneTodos) { item in TodoRow( item: item, onToggle: { model.toggleCompleted(id: item.id) }, onDelete: { model.deleteTodo(id: item.id) }, onUpdateTitle: { model.updateTitle(id: item.id, title: $0) } ) } } } } header: { Button { withAnimation(.easeInOut(duration: 0.15)) { isDoneExpanded.toggle() } } label: { HStack(spacing: 8) { Text("Done") Spacer() if doneTodos.count > 0 { Text("\(doneTodos.count)") .foregroundStyle(.secondary) } Image(systemName: isDoneExpanded ? "chevron.down" : "chevron.right") .foregroundStyle(.secondary) .font(.caption) } } .buttonStyle(.plain) } } .navigationTitle("Todos") .onAppear { lastDoneCount = doneTodos.count if lastDoneCount == 0 { isDoneExpanded = false } } .onChange(of: doneTodos.count) { newCount in if lastDoneCount == 0, newCount > 0, !isDoneExpanded { withAnimation(.easeInOut(duration: 0.15)) { isDoneExpanded = true } } else if newCount == 0, isDoneExpanded { withAnimation(.easeInOut(duration: 0.15)) { isDoneExpanded = false } } lastDoneCount = newCount } } } } private struct TodoRow: View { let item: TodoItem let onToggle: () -> Void let onDelete: () -> Void let onUpdateTitle: (String) -> Void @State private var draftTitle: String @FocusState private var isFocused: Bool init(item: TodoItem, onToggle: @escaping () -> Void, onDelete: @escaping () -> Void, onUpdateTitle: @escaping (String) -> Void) { self.item = item self.onToggle = onToggle self.onDelete = onDelete self.onUpdateTitle = onUpdateTitle _draftTitle = State(initialValue: item.title) } var body: some View { HStack(spacing: 12) { Button(action: onToggle) { Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle") .foregroundStyle(item.isCompleted ? .secondary : .primary) } .buttonStyle(.plain) .accessibilityLabel(item.isCompleted ? "Mark incomplete" : "Mark complete") TextField("Todo", text: $draftTitle) .focused($isFocused) .submitLabel(.done) .onSubmit { commitTitleIfNeeded() } .onChange(of: isFocused) { focused in if !focused { commitTitleIfNeeded() } } .onChange(of: item.title) { newValue in if !isFocused { draftTitle = newValue } } .foregroundStyle(item.isCompleted ? .secondary : .primary) .opacity(item.isCompleted ? 0.7 : 1.0) } .onDisappear { if isFocused { commitTitleIfNeeded() } } .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive, action: onDelete) { Label("Delete", systemImage: "trash") } } } private func commitTitleIfNeeded() { let trimmed = draftTitle.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { draftTitle = item.title return } guard trimmed != item.title else { return } onUpdateTitle(trimmed) } } struct TodosView_Previews: PreviewProvider { static var previews: some View { TodosView() } }