feat: add SwiftData persistence for lists and todos

This commit is contained in:
2026-02-12 18:36:01 +01:00
parent 7738f4b52f
commit 71af07eaf1
8 changed files with 102 additions and 33 deletions

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -1,6 +1,6 @@
import SwiftUI
enum Priority: Int, CaseIterable {
enum Priority: Int, CaseIterable, Codable {
case low = 0
case medium = 1
case high = 2

View File

@@ -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(),

View File

@@ -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

View File

@@ -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<TodoList>(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<TodoList>(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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -16,6 +16,7 @@ struct ListsOverviewView: View {
.foregroundStyle(.secondary)
}
}
.deleteDisabled(list.isInbox)
}
.onDelete(perform: deleteLists)