feat: extract reusable TodoListView and add tappable filter screens

This commit is contained in:
2026-02-12 22:30:47 +01:00
parent f642c502a0
commit 29719b7499
9 changed files with 199 additions and 31 deletions

View File

@@ -2,17 +2,29 @@ import SwiftUI
import SwiftData import SwiftData
struct ContentView: View { struct ContentView: View {
@State private var navigationPath: [UUID] = [] @State private var navigationPath: [Destination] = []
private var activeListID: UUID? {
if case .list(let id) = navigationPath.last {
return id
}
return nil
}
var body: some View { var body: some View {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
NavigationStack(path: $navigationPath) { NavigationStack(path: $navigationPath) {
ListsOverviewView() ListsOverviewView(navigationPath: $navigationPath)
.navigationDestination(for: UUID.self) { listID in .navigationDestination(for: Destination.self) { destination in
switch destination {
case .list(let listID):
ListDetailView(listID: listID) ListDetailView(listID: listID)
case .filter(let filter):
TodoListScreen(filter: filter)
} }
} }
MindDumpButton(activeListID: navigationPath.last) }
MindDumpButton(activeListID: activeListID)
} }
} }
} }

View File

@@ -0,0 +1,6 @@
import Foundation
enum Destination: Hashable {
case list(UUID)
case filter(TodoFilter)
}

View File

@@ -0,0 +1,29 @@
import Foundation
enum TodoFilter: Hashable {
case open, urgent, due
var title: String {
switch self {
case .open: "Offene Aufgaben"
case .urgent: "Dringende Aufgaben"
case .due: "Fällige Aufgaben"
}
}
var emptyMessage: String {
switch self {
case .open: "Keine offenen Aufgaben"
case .urgent: "Keine dringenden Aufgaben"
case .due: "Keine fälligen Aufgaben"
}
}
var emptyIcon: String {
switch self {
case .open: "checkmark.circle"
case .urgent: "exclamationmark.triangle"
case .due: "calendar.badge.checkmark"
}
}
}

View File

@@ -18,18 +18,30 @@ class ListStore {
var openCount: Int { allOpenItems.count } var openCount: Int { allOpenItems.count }
var urgentCount: Int { var urgentItems: [TodoItem] {
let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())! let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())!
return allOpenItems.filter { item in return allOpenItems.filter { item in
item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow) item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow)
}.count }
} }
var dueCount: Int { var urgentCount: Int { urgentItems.count }
var dueItems: [TodoItem] {
let startOfTomorrow = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) let startOfTomorrow = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!)
return allOpenItems.filter { item in return allOpenItems.filter { item in
item.deadline != nil && item.deadline! < startOfTomorrow item.deadline != nil && item.deadline! < startOfTomorrow
}.count }
}
var dueCount: Int { dueItems.count }
func items(for filter: TodoFilter) -> [TodoItem] {
switch filter {
case .open: allOpenItems
case .urgent: urgentItems
case .due: dueItems
}
} }
init(modelContext: ModelContext) { init(modelContext: ModelContext) {
@@ -106,6 +118,16 @@ class ListStore {
fetchLists() fetchLists()
} }
func toggleItemCompleted(_ item: TodoItem) {
guard let listID = item.list?.id else { return }
toggleItemCompleted(item.id, in: listID)
}
func deleteItem(_ item: TodoItem) {
guard let listID = item.list?.id else { return }
deleteItem(item.id, from: listID)
}
func moveItem(_ itemID: UUID, from sourceListID: UUID, to targetListID: UUID) { func moveItem(_ itemID: UUID, from sourceListID: UUID, to targetListID: UUID) {
guard sourceListID != targetListID, guard sourceListID != targetListID,
let sourceList = lists.first(where: { $0.id == sourceListID }), let sourceList = lists.first(where: { $0.id == sourceListID }),

View File

@@ -17,27 +17,21 @@ struct ListDetailView: View {
ContentUnavailableView("Keine Einträge", systemImage: "tray") ContentUnavailableView("Keine Einträge", systemImage: "tray")
} else { } else {
List { List {
ForEach(todoList.sortedItems) { item in TodoListView(
TodoRowView(item: item, onToggle: { items: todoList.sortedItems,
onToggle: { item in
store.toggleItemCompleted(item.id, in: listID) store.toggleItemCompleted(item.id, in: listID)
}, onTap: { },
onTap: { item in
editorItem = item editorItem = item
}) },
.swipeActions(edge: .leading) { onDelete: { item in
Button { store.deleteItem(item.id, from: listID)
},
onMove: { item in
moveItem = item moveItem = item
} label: {
Label("Verschieben", systemImage: "folder")
}
.tint(.blue)
}
}
.onDelete { offsets in
let sorted = todoList.sortedItems
for index in offsets {
store.deleteItem(sorted[index].id, from: listID)
}
} }
)
} }
} }
} }

View File

@@ -2,6 +2,7 @@ import SwiftUI
struct ListsOverviewView: View { struct ListsOverviewView: View {
@Environment(ListStore.self) private var store @Environment(ListStore.self) private var store
@Binding var navigationPath: [Destination]
@State private var newListName = "" @State private var newListName = ""
@FocusState private var isFieldFocused: Bool @FocusState private var isFieldFocused: Bool
@@ -9,16 +10,23 @@ struct ListsOverviewView: View {
List { List {
Section { Section {
HStack(spacing: 12) { HStack(spacing: 12) {
Button { navigationPath.append(.filter(.open)) } label: {
StatsCardView(icon: "checklist", count: store.openCount, label: "Offen", color: .blue) StatsCardView(icon: "checklist", count: store.openCount, label: "Offen", color: .blue)
}
Button { navigationPath.append(.filter(.urgent)) } label: {
StatsCardView(icon: "exclamationmark.triangle", count: store.urgentCount, label: "Dringend", color: .orange) StatsCardView(icon: "exclamationmark.triangle", count: store.urgentCount, label: "Dringend", color: .orange)
}
Button { navigationPath.append(.filter(.due)) } label: {
StatsCardView(icon: "calendar.badge.clock", count: store.dueCount, label: "Fällig", color: .red) StatsCardView(icon: "calendar.badge.clock", count: store.dueCount, label: "Fällig", color: .red)
} }
}
.buttonStyle(.plain)
.listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0)) .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
} }
ForEach(store.lists) { list in ForEach(store.lists) { list in
NavigationLink(value: list.id) { NavigationLink(value: Destination.list(list.id)) {
HStack { HStack {
Text(list.name) Text(list.name)
Spacer() Spacer()

View File

@@ -0,0 +1,50 @@
import SwiftUI
struct TodoListScreen: View {
@Environment(ListStore.self) private var store
let filter: TodoFilter
@State private var editorItem: TodoItem?
@State private var moveItem: TodoItem?
private var items: [TodoItem] {
store.items(for: filter)
}
var body: some View {
Group {
if items.isEmpty {
ContentUnavailableView(filter.emptyMessage, systemImage: filter.emptyIcon)
} else {
List {
TodoListView(
items: items,
showListName: true,
onToggle: { item in
store.toggleItemCompleted(item)
},
onTap: { item in
editorItem = item
},
onDelete: { item in
store.deleteItem(item)
},
onMove: { item in
moveItem = item
}
)
}
}
}
.navigationTitle(filter.title)
.sheet(item: $editorItem) { item in
if let listID = item.list?.id {
TodoEditorView(listID: listID, item: item)
}
}
.sheet(item: $moveItem) { item in
if let listID = item.list?.id {
MoveToListView(itemID: item.id, currentListID: listID)
}
}
}
}

View File

@@ -0,0 +1,40 @@
import SwiftUI
struct TodoListView: View {
let items: [TodoItem]
var showListName: Bool = false
var onToggle: (TodoItem) -> Void
var onTap: (TodoItem) -> Void
var onDelete: ((TodoItem) -> Void)?
var onMove: ((TodoItem) -> Void)?
var body: some View {
ForEach(items) { item in
TodoRowView(
item: item,
onToggle: { onToggle(item) },
subtitle: showListName ? item.list?.name : nil,
onTap: { onTap(item) }
)
.swipeActions(edge: .leading) {
if let onMove {
Button {
onMove(item)
} label: {
Label("Verschieben", systemImage: "folder")
}
.tint(.blue)
}
}
.swipeActions(edge: .trailing) {
if let onDelete {
Button(role: .destructive) {
onDelete(item)
} label: {
Label("Löschen", systemImage: "trash")
}
}
}
}
}
}

View File

@@ -3,6 +3,7 @@ import SwiftUI
struct TodoRowView: View { struct TodoRowView: View {
let item: TodoItem let item: TodoItem
let onToggle: () -> Void let onToggle: () -> Void
var subtitle: String?
var onTap: (() -> Void)? var onTap: (() -> Void)?
private var isOverdue: Bool { private var isOverdue: Bool {
@@ -33,6 +34,12 @@ struct TodoRowView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.lineLimit(1) .lineLimit(1)
} }
if let subtitle {
Text(subtitle)
.font(.caption2)
.foregroundStyle(.tertiary)
}
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle()) .contentShape(Rectangle())