From 71af07eaf181c522a4963b17720212304d336d1d Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 12 Feb 2026 18:36:01 +0100 Subject: [PATCH] feat: add SwiftData persistence for lists and todos --- MindDump/ContentView.swift | 8 ++- MindDump/MindDumpApp.swift | 10 ++- MindDump/Models/Priority.swift | 2 +- MindDump/Models/TodoItem.swift | 9 ++- MindDump/Models/TodoList.swift | 13 +++- MindDump/ViewModels/ListStore.swift | 85 +++++++++++++++++++------- MindDump/Views/ListDetailView.swift | 7 ++- MindDump/Views/ListsOverviewView.swift | 1 + 8 files changed, 102 insertions(+), 33 deletions(-) diff --git a/MindDump/ContentView.swift b/MindDump/ContentView.swift index 9a7292b..13a0e6d 100644 --- a/MindDump/ContentView.swift +++ b/MindDump/ContentView.swift @@ -1,4 +1,5 @@ import SwiftUI +import SwiftData struct ContentView: View { @State private var navigationPath: [UUID] = [] @@ -17,6 +18,11 @@ struct ContentView: View { } #Preview { + let container = try! ModelContainer( + for: TodoList.self, TodoItem.self, + configurations: ModelConfiguration(isStoredInMemoryOnly: true) + ) ContentView() - .environment(ListStore()) + .environment(ListStore(modelContext: container.mainContext)) + .modelContainer(container) } diff --git a/MindDump/MindDumpApp.swift b/MindDump/MindDumpApp.swift index 50d2ffe..d5f8de6 100644 --- a/MindDump/MindDumpApp.swift +++ b/MindDump/MindDumpApp.swift @@ -1,8 +1,16 @@ import SwiftUI +import SwiftData @main struct MindDumpApp: App { - @State private var store = ListStore() + private let container: ModelContainer + @State private var store: ListStore + + init() { + let container = try! ModelContainer(for: TodoList.self, TodoItem.self) + self.container = container + self._store = State(initialValue: ListStore(modelContext: container.mainContext)) + } var body: some Scene { WindowGroup { diff --git a/MindDump/Models/Priority.swift b/MindDump/Models/Priority.swift index 0d06570..fe6f595 100644 --- a/MindDump/Models/Priority.swift +++ b/MindDump/Models/Priority.swift @@ -1,6 +1,6 @@ import SwiftUI -enum Priority: Int, CaseIterable { +enum Priority: Int, CaseIterable, Codable { case low = 0 case medium = 1 case high = 2 diff --git a/MindDump/Models/TodoItem.swift b/MindDump/Models/TodoItem.swift index df71cbf..c662175 100644 --- a/MindDump/Models/TodoItem.swift +++ b/MindDump/Models/TodoItem.swift @@ -1,14 +1,17 @@ import Foundation +import SwiftData -struct TodoItem: Identifiable { - let id: UUID +@Model +class TodoItem: Identifiable { + private(set) var id: UUID var title: String var isCompleted: Bool - let createdAt: Date + private(set) var createdAt: Date var notes: String? var deadline: Date? var priority: Priority? var modifiedAt: Date? + var list: TodoList? init( id: UUID = UUID(), diff --git a/MindDump/Models/TodoList.swift b/MindDump/Models/TodoList.swift index a5eeb07..4d5cfda 100644 --- a/MindDump/Models/TodoList.swift +++ b/MindDump/Models/TodoList.swift @@ -1,10 +1,17 @@ import Foundation +import SwiftData -struct TodoList: Identifiable { - let id: UUID +@Model +class TodoList: Identifiable { + private(set) var id: UUID var name: String + @Relationship(deleteRule: .cascade, inverse: \TodoItem.list) var items: [TodoItem] - let isInbox: Bool + private(set) var isInbox: Bool + + var sortedItems: [TodoItem] { + items.sorted { $0.createdAt < $1.createdAt } + } init(id: UUID = UUID(), name: String, items: [TodoItem] = [], isInbox: Bool = false) { self.id = id diff --git a/MindDump/ViewModels/ListStore.swift b/MindDump/ViewModels/ListStore.swift index 41898ab..0f87f10 100644 --- a/MindDump/ViewModels/ListStore.swift +++ b/MindDump/ViewModels/ListStore.swift @@ -1,26 +1,35 @@ import Foundation import Observation +import SwiftData @Observable class ListStore { - var lists: [TodoList] + var lists: [TodoList] = [] + + private let modelContext: ModelContext var inboxID: UUID { lists.first { $0.isInbox }!.id } - init() { - self.lists = [TodoList(name: "Inbox", isInbox: true)] + init(modelContext: ModelContext) { + self.modelContext = modelContext + ensureInbox() + fetchLists() } func addList(name: String) { let list = TodoList(name: name) - lists.append(list) + modelContext.insert(list) + save() + fetchLists() } func deleteList(_ list: TodoList) { guard !list.isInbox else { return } - lists.removeAll { $0.id == list.id } + modelContext.delete(list) + save() + fetchLists() } func addItem( @@ -30,14 +39,19 @@ class ListStore { deadline: Date? = nil, priority: Priority? = nil ) { - guard let index = lists.firstIndex(where: { $0.id == listID }) else { return } + guard let list = lists.first(where: { $0.id == listID }) else { return } let item = TodoItem(title: title, notes: notes, deadline: deadline, priority: priority) - lists[index].items.append(item) + list.items.append(item) + save() + fetchLists() } func deleteItem(_ itemID: UUID, from listID: UUID) { - guard let listIndex = lists.firstIndex(where: { $0.id == listID }) else { return } - lists[listIndex].items.removeAll { $0.id == itemID } + guard let list = lists.first(where: { $0.id == listID }), + let item = list.items.first(where: { $0.id == itemID }) else { return } + modelContext.delete(item) + save() + fetchLists() } func updateItem( @@ -49,22 +63,51 @@ class ListStore { priority: Priority? = nil, isCompleted: Bool? = nil ) { - guard let listIndex = lists.firstIndex(where: { $0.id == listID }), - let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return } - lists[listIndex].items[itemIndex].title = title - lists[listIndex].items[itemIndex].notes = notes - lists[listIndex].items[itemIndex].deadline = deadline - lists[listIndex].items[itemIndex].priority = priority + guard let list = lists.first(where: { $0.id == listID }), + let item = list.items.first(where: { $0.id == itemID }) else { return } + item.title = title + item.notes = notes + item.deadline = deadline + item.priority = priority if let isCompleted { - lists[listIndex].items[itemIndex].isCompleted = isCompleted + item.isCompleted = isCompleted } - lists[listIndex].items[itemIndex].modifiedAt = Date() + item.modifiedAt = Date() + save() + fetchLists() } func toggleItemCompleted(_ itemID: UUID, in listID: UUID) { - guard let listIndex = lists.firstIndex(where: { $0.id == listID }), - let itemIndex = lists[listIndex].items.firstIndex(where: { $0.id == itemID }) else { return } - lists[listIndex].items[itemIndex].isCompleted.toggle() - lists[listIndex].items[itemIndex].modifiedAt = Date() + guard let list = lists.first(where: { $0.id == listID }), + let item = list.items.first(where: { $0.id == itemID }) else { return } + item.isCompleted.toggle() + item.modifiedAt = Date() + save() + fetchLists() + } + + // MARK: - Private + + private func ensureInbox() { + let descriptor = FetchDescriptor(predicate: #Predicate { $0.isInbox }) + let count = (try? modelContext.fetchCount(descriptor)) ?? 0 + if count == 0 { + let inbox = TodoList(name: "Inbox", isInbox: true) + modelContext.insert(inbox) + save() + } + } + + private func fetchLists() { + let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.name)]) + let fetched = (try? modelContext.fetch(descriptor)) ?? [] + // Inbox always first + let inbox = fetched.filter { $0.isInbox } + let rest = fetched.filter { !$0.isInbox } + lists = inbox + rest + } + + private func save() { + try? modelContext.save() } } diff --git a/MindDump/Views/ListDetailView.swift b/MindDump/Views/ListDetailView.swift index e0516f6..a4b4816 100644 --- a/MindDump/Views/ListDetailView.swift +++ b/MindDump/Views/ListDetailView.swift @@ -12,11 +12,11 @@ struct ListDetailView: View { var body: some View { Group { if let todoList { - if todoList.items.isEmpty { + if todoList.sortedItems.isEmpty { ContentUnavailableView("Keine Einträge", systemImage: "tray") } else { List { - ForEach(todoList.items) { item in + ForEach(todoList.sortedItems) { item in TodoRowView(item: item, onToggle: { store.toggleItemCompleted(item.id, in: listID) }, onTap: { @@ -24,8 +24,9 @@ struct ListDetailView: View { }) } .onDelete { offsets in + let sorted = todoList.sortedItems for index in offsets { - store.deleteItem(todoList.items[index].id, from: listID) + store.deleteItem(sorted[index].id, from: listID) } } } diff --git a/MindDump/Views/ListsOverviewView.swift b/MindDump/Views/ListsOverviewView.swift index 448eb51..d623672 100644 --- a/MindDump/Views/ListsOverviewView.swift +++ b/MindDump/Views/ListsOverviewView.swift @@ -16,6 +16,7 @@ struct ListsOverviewView: View { .foregroundStyle(.secondary) } } + .deleteDisabled(list.isInbox) } .onDelete(perform: deleteLists)