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
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 {
ZStack(alignment: .bottomTrailing) {
NavigationStack(path: $navigationPath) {
ListsOverviewView()
.navigationDestination(for: UUID.self) { listID in
ListsOverviewView(navigationPath: $navigationPath)
.navigationDestination(for: Destination.self) { destination in
switch destination {
case .list(let 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 urgentCount: Int {
var urgentItems: [TodoItem] {
let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())!
return allOpenItems.filter { item in
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())!)
return allOpenItems.filter { item in
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) {
@@ -106,6 +118,16 @@ class ListStore {
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) {
guard sourceListID != targetListID,
let sourceList = lists.first(where: { $0.id == sourceListID }),

View File

@@ -17,27 +17,21 @@ struct ListDetailView: View {
ContentUnavailableView("Keine Einträge", systemImage: "tray")
} else {
List {
ForEach(todoList.sortedItems) { item in
TodoRowView(item: item, onToggle: {
TodoListView(
items: todoList.sortedItems,
onToggle: { item in
store.toggleItemCompleted(item.id, in: listID)
}, onTap: {
},
onTap: { item in
editorItem = item
})
.swipeActions(edge: .leading) {
Button {
},
onDelete: { item in
store.deleteItem(item.id, from: listID)
},
onMove: { item in
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 {
@Environment(ListStore.self) private var store
@Binding var navigationPath: [Destination]
@State private var newListName = ""
@FocusState private var isFieldFocused: Bool
@@ -9,16 +10,23 @@ struct ListsOverviewView: View {
List {
Section {
HStack(spacing: 12) {
Button { navigationPath.append(.filter(.open)) } label: {
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)
}
Button { navigationPath.append(.filter(.due)) } label: {
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))
.listRowBackground(Color.clear)
}
ForEach(store.lists) { list in
NavigationLink(value: list.id) {
NavigationLink(value: Destination.list(list.id)) {
HStack {
Text(list.name)
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 {
let item: TodoItem
let onToggle: () -> Void
var subtitle: String?
var onTap: (() -> Void)?
private var isOverdue: Bool {
@@ -33,6 +34,12 @@ struct TodoRowView: View {
.foregroundStyle(.secondary)
.lineLimit(1)
}
if let subtitle {
Text(subtitle)
.font(.caption2)
.foregroundStyle(.tertiary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())