feat: add SwiftData persistence for lists and todos
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
enum Priority: Int, CaseIterable {
|
||||
enum Priority: Int, CaseIterable, Codable {
|
||||
case low = 0
|
||||
case medium = 1
|
||||
case high = 2
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ struct ListsOverviewView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.deleteDisabled(list.isInbox)
|
||||
}
|
||||
.onDelete(perform: deleteLists)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user