fix: add inline comments to models, viewmodel and views for better readable code
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user