Listless/Models/ItemStore.swift (6920B)
1import CoreData
2import Foundation
3
4enum ItemStoreError: LocalizedError {
5 case fetchFailed(Error)
6 case saveFailed(Error)
7
8 var errorDescription: String? {
9 switch self {
10 case .fetchFailed(let error):
11 return "Failed to fetch items: \(error.localizedDescription)"
12 case .saveFailed(let error):
13 return "Failed to save changes: \(error.localizedDescription)"
14 }
15 }
16}
17
18@MainActor
19final class ItemStore {
20 private let persistenceController: PersistenceController
21 private var context: NSManagedObjectContext {
22 persistenceController.viewContext
23 }
24
25 init(persistenceController: PersistenceController = .shared) {
26 self.persistenceController = persistenceController
27 }
28
29 func fetchItems() throws -> [ItemEntity] {
30 do {
31 let activeRequest = ItemEntity.fetchRequest()
32 activeRequest.predicate = NSPredicate(format: "completedOrder == 0")
33 activeRequest.sortDescriptors = [
34 NSSortDescriptor(keyPath: \ItemEntity.sortOrder, ascending: true)
35 ]
36
37 let completedRequest = ItemEntity.fetchRequest()
38 completedRequest.predicate = NSPredicate(format: "completedOrder > 0")
39 completedRequest.sortDescriptors = [
40 NSSortDescriptor(keyPath: \ItemEntity.completedOrder, ascending: false)
41 ]
42
43 let activeItems = try context.fetch(activeRequest)
44 let completedItems = try context.fetch(completedRequest)
45
46 return activeItems + completedItems
47 } catch {
48 throw ItemStoreError.fetchFailed(error)
49 }
50 }
51
52 @discardableResult
53 func createItem(title: String = "", atBeginning: Bool = false, sortOrder: Int64? = nil) throws
54 -> ItemEntity
55 {
56
57
58 let resolvedSortOrder: Int64
59 if let sortOrder {
60 resolvedSortOrder = sortOrder
61 } else if atBeginning {
62 let minOrder = try minActiveSortOrder() ?? 0
63 resolvedSortOrder = minOrder - 1000
64 } else {
65 let maxOrder = try maxActiveSortOrder() ?? -1000
66 resolvedSortOrder = maxOrder + 1000
67 }
68
69 let item = ItemEntity(context: context)
70 item.title = title
71 item.sortOrder = resolvedSortOrder
72
73 return item
74 }
75
76 private func minActiveSortOrder() throws -> Int64? {
77 let request = ItemEntity.fetchRequest()
78 request.predicate = NSPredicate(format: "completedOrder == 0")
79 request.sortDescriptors = [
80 NSSortDescriptor(keyPath: \ItemEntity.sortOrder, ascending: true)
81 ]
82 request.fetchLimit = 1
83 do {
84 return try context.fetch(request).first?.sortOrder
85 } catch {
86 throw ItemStoreError.fetchFailed(error)
87 }
88 }
89
90 private func maxActiveSortOrder() throws -> Int64? {
91 let request = ItemEntity.fetchRequest()
92 request.predicate = NSPredicate(format: "completedOrder == 0")
93 request.sortDescriptors = [
94 NSSortDescriptor(keyPath: \ItemEntity.sortOrder, ascending: false)
95 ]
96 request.fetchLimit = 1
97 do {
98 return try context.fetch(request).first?.sortOrder
99 } catch {
100 throw ItemStoreError.fetchFailed(error)
101 }
102 }
103
104 func complete(itemID: UUID) throws {
105 guard let item = try findItem(id: itemID) else { return }
106
107 let request = ItemEntity.fetchRequest()
108 request.predicate = NSPredicate(format: "completedOrder > 0")
109 request.sortDescriptors = [
110 NSSortDescriptor(keyPath: \ItemEntity.completedOrder, ascending: false)
111 ]
112 request.fetchLimit = 1
113 let maxOrder = (try context.fetch(request).first)?.completedOrder ?? 0
114
115 item.completedOrder = maxOrder + 1
116 try save()
117 }
118
119 func uncomplete(itemID: UUID) throws {
120 guard let item = try findItem(id: itemID) else { return }
121 let restoredSortOrder = item.sortOrder
122 let activeItems = try fetchItems().filter { !$0.isCompleted && $0.id != item.id }
123 let hasSortOrderConflict = activeItems.contains { $0.sortOrder == restoredSortOrder }
124
125 if hasSortOrderConflict {
126 let maxSortOrder = activeItems.map(\.sortOrder).max() ?? -1000
127 item.sortOrder = maxSortOrder + 1000
128 }
129
130 item.completedOrder = 0
131 try save()
132 }
133
134 func update(itemID: UUID, title: String) throws {
135 guard let item = try findItem(id: itemID) else { return }
136 item.title = title
137 try save()
138 }
139
140 func updateWithoutSaving(itemID: UUID, title: String) throws {
141 guard let item = try findItem(id: itemID) else { return }
142 item.title = title
143
144 }
145
146 func delete(itemID: UUID) throws {
147 guard let item = try findItem(id: itemID) else { return }
148 context.delete(item)
149 try save()
150 }
151
152 func deleteMultiple(itemIDs: [UUID]) throws {
153 for itemID in itemIDs {
154 guard let item = try findItem(id: itemID) else { continue }
155 context.delete(item)
156 }
157 try save()
158 }
159
160 func normalizeSortOrders() throws {
161 let activeItems = try fetchItems().filter { !$0.isCompleted }
162 .sorted { $0.sortOrder < $1.sortOrder }
163
164 for (index, item) in activeItems.enumerated() {
165 item.sortOrder = Int64(index) * 1000
166 }
167
168 try save()
169 }
170
171 func moveItem(itemID: UUID, toIndex: Int) throws {
172 let activeItems = try fetchItems().filter { !$0.isCompleted }
173 .sorted { $0.sortOrder < $1.sortOrder }
174
175 guard let currentIndex = activeItems.firstIndex(where: { $0.id == itemID }) else { return }
176 guard currentIndex != toIndex else { return }
177
178 var reordered = activeItems
179 let item = reordered.remove(at: currentIndex)
180
181
182 let insertIndex = max(0, min(toIndex, reordered.count))
183 reordered.insert(item, at: insertIndex)
184
185
186 for (index, item) in reordered.enumerated() {
187 item.sortOrder = Int64(index) * 1000
188 }
189
190 try save()
191 }
192
193 private func findItem(id: UUID) throws -> ItemEntity? {
194 let request = ItemEntity.fetchRequest()
195 request.predicate = NSPredicate(format: "id == %@", id as CVarArg)
196 request.fetchLimit = 1
197
198 do {
199 return try context.fetch(request).first
200 } catch {
201 throw ItemStoreError.fetchFailed(error)
202 }
203 }
204
205 func save() throws {
206 try persistenceController.save()
207 }
208}