fix: add inline comments to models, viewmodel and views for better readable code

This commit is contained in:
2026-03-04 19:30:06 +01:00
parent 76bba19959
commit 9747b2ea67
7 changed files with 29 additions and 9 deletions

View File

@@ -7,6 +7,7 @@ struct MindDumpApp: App {
@State private var store: ListStore @State private var store: ListStore
init() { init() {
// Use in-memory store for UI tests so they start with a clean database
let isUITest = ProcessInfo.processInfo.arguments.contains("UI_TESTING") let isUITest = ProcessInfo.processInfo.arguments.contains("UI_TESTING")
let config = isUITest let config = isUITest
? ModelConfiguration(isStoredInMemoryOnly: true) ? ModelConfiguration(isStoredInMemoryOnly: true)

View File

@@ -13,6 +13,7 @@ class TodoItem: Identifiable {
var modifiedAt: Date? var modifiedAt: Date?
var list: TodoList? var list: TodoList?
// Overdue if deadline is before start of tomorrow and item is not completed
var isOverdue: Bool { var isOverdue: Bool {
guard !isCompleted, let deadline else { return false } guard !isCompleted, let deadline else { return false }
return deadline < Calendar.current.startOfDay( return deadline < Calendar.current.startOfDay(

View File

@@ -5,6 +5,7 @@ import SwiftData
class TodoList: Identifiable { class TodoList: Identifiable {
private(set) var id: UUID private(set) var id: UUID
var name: String var name: String
// Cascade delete: removing a list also removes all its items
@Relationship(deleteRule: .cascade, inverse: \TodoItem.list) @Relationship(deleteRule: .cascade, inverse: \TodoItem.list)
var items: [TodoItem] var items: [TodoItem]
private(set) var isInbox: Bool private(set) var isInbox: Bool

View File

@@ -2,16 +2,20 @@ import Foundation
import Observation import Observation
import SwiftData import SwiftData
// Central ViewModel that manages all lists and items via SwiftData.
// Injected into the view hierarchy as @Environment(ListStore.self).
@Observable @Observable
class ListStore { class ListStore {
var lists: [TodoList] = [] var lists: [TodoList] = []
private let modelContext: ModelContext private let modelContext: ModelContext
// Convenience accessor Inbox is guaranteed to exist after init
var inboxID: UUID { var inboxID: UUID {
lists.first { $0.isInbox }!.id lists.first { $0.isInbox }!.id
} }
// All incomplete items across all lists
var allOpenItems: [TodoItem] { var allOpenItems: [TodoItem] {
lists.flatMap { $0.items }.filter { !$0.isCompleted } lists.flatMap { $0.items }.filter { !$0.isCompleted }
} }
@@ -30,6 +34,7 @@ class ListStore {
var dueCount: Int { dueItems.count } var dueCount: Int { dueItems.count }
// All completed items across all lists, newest first
var allCompletedItems: [TodoItem] { var allCompletedItems: [TodoItem] {
lists.flatMap { $0.items } lists.flatMap { $0.items }
.filter { $0.isCompleted } .filter { $0.isCompleted }
@@ -49,6 +54,7 @@ class ListStore {
fetchLists() fetchLists()
} }
// Inbox cannot be deleted
func deleteList(_ list: TodoList) { func deleteList(_ list: TodoList) {
guard !list.isInbox else { return } guard !list.isInbox else { return }
modelContext.delete(list) modelContext.delete(list)
@@ -120,6 +126,7 @@ class ListStore {
deleteItem(item.id, from: listID) deleteItem(item.id, from: listID)
} }
// Filters open items by urgency/due status and sorts them by the given strategy
func filteredOpenItems(for list: TodoList? = nil, urgent: Bool, due: Bool, sort: ListDetailSort) -> [TodoItem] { func filteredOpenItems(for list: TodoList? = nil, urgent: Bool, due: Bool, sort: ListDetailSort) -> [TodoItem] {
var items = list?.openItems ?? allOpenItems var items = list?.openItems ?? allOpenItems
@@ -137,6 +144,7 @@ class ListStore {
case .newest: case .newest:
items.sort { $0.createdAt > $1.createdAt } items.sort { $0.createdAt > $1.createdAt }
case .dueDate: case .dueDate:
// Items with deadline first (earliest on top), items without deadline at the end
items.sort { lhs, rhs in items.sort { lhs, rhs in
switch (lhs.deadline, rhs.deadline) { switch (lhs.deadline, rhs.deadline) {
case let (l?, r?): l < r case let (l?, r?): l < r
@@ -146,6 +154,7 @@ class ListStore {
} }
} }
case .priority: case .priority:
// Highest priority first, fallback to createdAt for same priority
items.sort { lhs, rhs in items.sort { lhs, rhs in
let lp = lhs.priority?.rawValue ?? -1 let lp = lhs.priority?.rawValue ?? -1
let rp = rhs.priority?.rawValue ?? -1 let rp = rhs.priority?.rawValue ?? -1
@@ -157,6 +166,7 @@ class ListStore {
return items return items
} }
// Moves an item from one list to another and updates its timestamp
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 }),
@@ -169,6 +179,7 @@ class ListStore {
fetchLists() fetchLists()
} }
// Creates the Inbox list on first launch if it doesn't exist yet
private func ensureInbox() { private func ensureInbox() {
let descriptor = FetchDescriptor<TodoList>(predicate: #Predicate { $0.isInbox }) let descriptor = FetchDescriptor<TodoList>(predicate: #Predicate { $0.isInbox })
let count = (try? modelContext.fetchCount(descriptor)) ?? 0 let count = (try? modelContext.fetchCount(descriptor)) ?? 0
@@ -179,6 +190,7 @@ class ListStore {
} }
} }
// Re-fetches all lists from SwiftData, keeping Inbox at the top
private func fetchLists() { private func fetchLists() {
let descriptor = FetchDescriptor<TodoList>(sortBy: [SortDescriptor(\.name)]) let descriptor = FetchDescriptor<TodoList>(sortBy: [SortDescriptor(\.name)])
let fetched = (try? modelContext.fetch(descriptor)) ?? [] let fetched = (try? modelContext.fetch(descriptor)) ?? []
@@ -188,6 +200,7 @@ class ListStore {
lists = inbox + rest lists = inbox + rest
} }
// Urgent = high priority OR deadline within 3 days
private func isUrgent(_ item: TodoItem) -> Bool { private func isUrgent(_ item: TodoItem) -> Bool {
let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())! let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())!
return item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow) return item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow)

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
// Floating action button for quick-dump. Adds to the current list or falls back to Inbox.
struct MindDumpButton: View { struct MindDumpButton: View {
@Environment(ListStore.self) private var store @Environment(ListStore.self) private var store
var activeListID: UUID? var activeListID: UUID?

View File

@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
// Shared editor for creating and editing todos. Used by MindDumpButton and list views.
struct TodoEditorView: View { struct TodoEditorView: View {
@Environment(ListStore.self) private var store @Environment(ListStore.self) private var store
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@@ -107,6 +108,7 @@ struct TodoEditorView: View {
} }
} }
.onAppear { .onAppear {
// When editing, pre-fill fields and show sections that have values
if let item { if let item {
title = item.title title = item.title
@@ -152,6 +154,7 @@ struct TodoEditorView: View {
} }
} }
// Saves the entry creates a new item or updates the existing one
private func save() { private func save() {
let trimmed = title.trimmingCharacters(in: .whitespaces) let trimmed = title.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return } guard !trimmed.isEmpty else { return }

View File

@@ -6,14 +6,14 @@ final class TodoItemTests: XCTestCase {
// MARK: - isOverdue // MARK: - isOverdue
/// Past deadline should be overdue // Past deadline should be overdue
func testIsOverdue_deadlineYesterday_returnsTrue() { func testIsOverdue_deadlineYesterday_returnsTrue() {
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())! let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
let item = TodoItem(title: "Test", deadline: yesterday) let item = TodoItem(title: "Test", deadline: yesterday)
XCTAssertTrue(item.isOverdue) XCTAssertTrue(item.isOverdue)
} }
/// Completed items are never overdue, even with past deadline // Completed items are never overdue, even with past deadline
func testIsOverdue_completedWithPastDeadline_returnsFalse() { func testIsOverdue_completedWithPastDeadline_returnsFalse() {
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())! let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
let item = TodoItem(title: "Test", isCompleted: true, deadline: yesterday) let item = TodoItem(title: "Test", isCompleted: true, deadline: yesterday)
@@ -27,7 +27,7 @@ final class TodoListTests: XCTestCase {
// MARK: - openItems / completedItems // MARK: - openItems / completedItems
/// Items are correctly split into open and completed // Items are correctly split into open and completed
func testOpenAndCompletedItems_correctSplit() { func testOpenAndCompletedItems_correctSplit() {
let open1 = TodoItem(title: "Open 1") let open1 = TodoItem(title: "Open 1")
let open2 = TodoItem(title: "Open 2") let open2 = TodoItem(title: "Open 2")
@@ -40,7 +40,7 @@ final class TodoListTests: XCTestCase {
XCTAssertEqual(list.completedItems.first?.title, "Done") XCTAssertEqual(list.completedItems.first?.title, "Done")
} }
/// Open items are sorted oldest first // Open items are sorted oldest first
func testOpenItems_sortedByCreatedAtAscending() { func testOpenItems_sortedByCreatedAtAscending() {
let older = TodoItem(title: "Older", createdAt: Date().addingTimeInterval(-100)) let older = TodoItem(title: "Older", createdAt: Date().addingTimeInterval(-100))
let newer = TodoItem(title: "Newer", createdAt: Date()) let newer = TodoItem(title: "Newer", createdAt: Date())
@@ -50,7 +50,7 @@ final class TodoListTests: XCTestCase {
XCTAssertEqual(list.openItems.map(\.title), ["Older", "Newer"]) XCTAssertEqual(list.openItems.map(\.title), ["Older", "Newer"])
} }
/// Completed items are sorted newest first (by modifiedAt) // Completed items are sorted newest first (by modifiedAt)
func testCompletedItems_sortedByModifiedAtDescending() { func testCompletedItems_sortedByModifiedAtDescending() {
let earlier = TodoItem(title: "Earlier", isCompleted: true, modifiedAt: Date().addingTimeInterval(-100)) let earlier = TodoItem(title: "Earlier", isCompleted: true, modifiedAt: Date().addingTimeInterval(-100))
let later = TodoItem(title: "Later", isCompleted: true, modifiedAt: Date()) let later = TodoItem(title: "Later", isCompleted: true, modifiedAt: Date())
@@ -78,7 +78,7 @@ final class ListStoreTests: XCTestCase {
// MARK: - addItem / deleteItem // MARK: - addItem / deleteItem
/// Adding an item increases the list count and stores the title // Adding an item increases the list count and stores the title
func testAddItem_increasesCount() { func testAddItem_increasesCount() {
let inboxID = store.inboxID let inboxID = store.inboxID
store.addItem(to: inboxID, title: "Buy milk") store.addItem(to: inboxID, title: "Buy milk")
@@ -87,7 +87,7 @@ final class ListStoreTests: XCTestCase {
XCTAssertEqual(store.lists.first!.items.first?.title, "Buy milk") XCTAssertEqual(store.lists.first!.items.first?.title, "Buy milk")
} }
/// Deleting an item removes it from the list // Deleting an item removes it from the list
func testDeleteItem_removesItem() { func testDeleteItem_removesItem() {
let inboxID = store.inboxID let inboxID = store.inboxID
store.addItem(to: inboxID, title: "Temp") store.addItem(to: inboxID, title: "Temp")
@@ -100,7 +100,7 @@ final class ListStoreTests: XCTestCase {
// MARK: - toggleItemCompleted // MARK: - toggleItemCompleted
/// Toggling flips isCompleted back and forth // Toggling flips isCompleted back and forth
func testToggleItemCompleted_flipsStatus() { func testToggleItemCompleted_flipsStatus() {
let inboxID = store.inboxID let inboxID = store.inboxID
store.addItem(to: inboxID, title: "Task") store.addItem(to: inboxID, title: "Task")
@@ -117,7 +117,7 @@ final class ListStoreTests: XCTestCase {
// MARK: - moveItem // MARK: - moveItem
/// Moving an item removes it from the source list, adds it to the target list, and updates modifiedAt // Moving an item removes it from the source list, adds it to the target list, and updates modifiedAt
func testMoveItem_movesAcrossLists() { func testMoveItem_movesAcrossLists() {
let inboxID = store.inboxID let inboxID = store.inboxID
store.addList(name: "Arbeit") store.addList(name: "Arbeit")