DataTableStateModel.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. import CoreData
  2. import HealthKit
  3. import Observation
  4. import SwiftUI
  5. extension DataTable {
  6. @Observable final class StateModel: BaseStateModel<Provider> {
  7. @ObservationIgnored @Injected() var broadcaster: Broadcaster!
  8. @ObservationIgnored @Injected() var apsManager: APSManager!
  9. @ObservationIgnored @Injected() var unlockmanager: UnlockManager!
  10. @ObservationIgnored @Injected() private var storage: FileStorage!
  11. @ObservationIgnored @Injected() var pumpHistoryStorage: PumpHistoryStorage!
  12. @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
  13. @ObservationIgnored @Injected() var healthKitManager: HealthKitManager!
  14. @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
  15. let coredataContext = CoreDataStack.shared.newTaskContext()
  16. var mode: Mode = .treatments
  17. var treatments: [Treatment] = []
  18. var manualGlucose: Decimal = 0
  19. var waitForSuggestion: Bool = false
  20. var insulinEntryDeleted: Bool = false
  21. var carbEntryDeleted: Bool = false
  22. var units: GlucoseUnits = .mgdL
  23. var carbEntryToEdit: CarbEntryStored?
  24. var showCarbEntryEditor = false
  25. override func subscribe() {
  26. units = settingsManager.settings.units
  27. broadcaster.register(DeterminationObserver.self, observer: self)
  28. broadcaster.register(SettingsObserver.self, observer: self)
  29. }
  30. /// Checks if the glucose data is fresh based on the given date
  31. /// - Parameter glucoseDate: The date to check
  32. /// - Returns: Boolean indicating if the data is fresh
  33. func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
  34. glucoseStorage.isGlucoseDataFresh(glucoseDate)
  35. }
  36. /// Initiates the glucose deletion process asynchronously
  37. /// - Parameter treatmentObjectID: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  38. func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  39. Task {
  40. await deleteGlucose(treatmentObjectID)
  41. }
  42. }
  43. func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
  44. // Delete from Apple Health/Tidepool
  45. await deleteGlucoseFromServices(treatmentObjectID)
  46. // Delete from Core Data
  47. await glucoseStorage.deleteGlucose(treatmentObjectID)
  48. }
  49. func deleteGlucoseFromServices(_ treatmentObjectID: NSManagedObjectID) async {
  50. let taskContext = CoreDataStack.shared.newTaskContext()
  51. taskContext.name = "deleteContext"
  52. taskContext.transactionAuthor = "deleteGlucoseFromServices"
  53. await taskContext.perform {
  54. do {
  55. let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
  56. guard let glucoseToDelete = result else {
  57. debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
  58. return
  59. }
  60. // Delete from Nightscout
  61. if let id = glucoseToDelete.id?.uuidString {
  62. self.provider.deleteManualGlucoseFromNightscout(withID: id)
  63. }
  64. // Delete from Apple Health
  65. if let id = glucoseToDelete.id?.uuidString {
  66. self.provider.deleteGlucoseFromHealth(withSyncID: id)
  67. }
  68. debugPrint(
  69. "\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from remote service(s) (Nightscout, Apple Health, Tidepool)"
  70. )
  71. } catch {
  72. debugPrint(
  73. "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
  74. )
  75. }
  76. }
  77. }
  78. func addManualGlucose() {
  79. // Always save value in mg/dL
  80. let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
  81. let glucoseAsInt = Int(glucose)
  82. glucoseStorage.addManualGlucose(glucose: glucoseAsInt)
  83. }
  84. // Carb and FPU deletion from history
  85. /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  86. func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
  87. Task {
  88. await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
  89. await MainActor.run {
  90. carbEntryDeleted = true
  91. waitForSuggestion = true
  92. }
  93. }
  94. }
  95. func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async {
  96. // Delete from Nightscout/Apple Health/Tidepool
  97. await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
  98. // Delete carbs from Core Data
  99. await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
  100. // Perform a determine basal sync to update cob
  101. await apsManager.determineBasalSync()
  102. }
  103. /// Deletes carb and FPU entries from all connected services (Nightscout, HealthKit, Tidepool)
  104. /// - Parameters:
  105. /// - treatmentObjectID: The Core Data object ID of the entry to delete
  106. /// - isFPUDeletion: Flag indicating if this is a FPU deletion that requires special handling
  107. /// - If true: Will first fetch the corresponding carb entry and then delete both FPU and carb entries
  108. /// - If false: Will delete the entry directly as a standard carb deletion
  109. /// - Note: This function handles three scenarios:
  110. /// 1. Standard carb deletion (isFPUDeletion = false)
  111. /// 2. FPU-only deletion (isFPUDeletion = true)
  112. /// 3. Combined carb+FPU deletion (isFPUDeletion = true)
  113. func deleteFromServices(_ treatmentObjectID: NSManagedObjectID, isFPUDeletion: Bool = false) async {
  114. let taskContext = CoreDataStack.shared.newTaskContext()
  115. taskContext.name = "deleteContext"
  116. taskContext.transactionAuthor = "deleteCarbsFromServices"
  117. var carbEntry: CarbEntryStored?
  118. var objectIDToDelete = treatmentObjectID
  119. // For FPU deletions, first get the corresponding carb entry
  120. if isFPUDeletion {
  121. guard let correspondingEntry: (
  122. entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String)?,
  123. entryID: NSManagedObjectID?
  124. ) = await handleFPUEntry(treatmentObjectID),
  125. let nsManagedObjectID = correspondingEntry.entryID
  126. else { return }
  127. objectIDToDelete = nsManagedObjectID
  128. }
  129. // Delete entries from all services
  130. await taskContext.perform {
  131. do {
  132. carbEntry = try taskContext.existingObject(with: objectIDToDelete) as? CarbEntryStored
  133. guard let carbEntry = carbEntry else {
  134. debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
  135. return
  136. }
  137. // Delete FPU related entries if they exist
  138. if let fpuID = carbEntry.fpuID {
  139. // Delete Fat and Protein entries from Nightscout
  140. self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
  141. // Delete Fat and Protein entries from Apple Health
  142. let healthObjectsToDelete: [HKSampleType?] = [
  143. AppleHealthConfig.healthFatObject,
  144. AppleHealthConfig.healthProteinObject
  145. ]
  146. for sampleType in healthObjectsToDelete {
  147. if let validSampleType = sampleType {
  148. self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
  149. }
  150. }
  151. }
  152. // Delete carb entries if they exist
  153. if let id = carbEntry.id, let entryDate = carbEntry.date {
  154. self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
  155. // Delete carbs from Apple Health
  156. if let sampleType = AppleHealthConfig.healthCarbObject {
  157. self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
  158. }
  159. self.provider.deleteCarbsFromTidepool(
  160. withSyncId: id,
  161. carbs: Decimal(carbEntry.carbs),
  162. at: entryDate,
  163. enteredBy: CarbsEntry.local
  164. )
  165. }
  166. } catch {
  167. debugPrint("\(DebuggingIdentifiers.failed) Error deleting entries: \(error.localizedDescription)")
  168. }
  169. }
  170. }
  171. // Insulin deletion from history
  172. /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
  173. func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
  174. Task {
  175. await invokeInsulinDeletion(treatmentObjectID)
  176. await MainActor.run {
  177. insulinEntryDeleted = true
  178. waitForSuggestion = true
  179. }
  180. }
  181. }
  182. func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async {
  183. do {
  184. let authenticated = try await unlockmanager.unlock()
  185. guard authenticated else {
  186. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Authentication Error")
  187. return
  188. }
  189. // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
  190. await deleteInsulinFromServices(with: treatmentObjectID)
  191. // Delete from Core Data
  192. await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
  193. // Perform a determine basal sync to update iob
  194. await apsManager.determineBasalSync()
  195. } catch {
  196. debugPrint(
  197. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
  198. )
  199. }
  200. }
  201. func deleteInsulinFromServices(with treatmentObjectID: NSManagedObjectID) async {
  202. let taskContext = CoreDataStack.shared.newTaskContext()
  203. taskContext.name = "deleteContext"
  204. taskContext.transactionAuthor = "deleteInsulinFromServices"
  205. await taskContext.perform {
  206. do {
  207. guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
  208. else {
  209. debug(.default, "Could not cast the object to PumpEventStored")
  210. return
  211. }
  212. if let id = treatmentToDelete.id, let timestamp = treatmentToDelete.timestamp,
  213. let bolus = treatmentToDelete.bolus, let bolusAmount = bolus.amount
  214. {
  215. self.provider.deleteInsulinFromNightscout(withID: id)
  216. self.provider.deleteInsulinFromHealth(withSyncID: id)
  217. self.provider.deleteInsulinFromTidepool(withSyncId: id, amount: bolusAmount as Decimal, at: timestamp)
  218. }
  219. taskContext.delete(treatmentToDelete)
  220. try taskContext.save()
  221. debug(.default, "Successfully deleted the treatment object.")
  222. } catch {
  223. debug(.default, "Failed to delete the treatment object: \(error.localizedDescription)")
  224. }
  225. }
  226. }
  227. // MARK: - Entry Management
  228. /// Updates a carb/FPU entry with new values and handles the necessary cleanup and recreation of FPU entries
  229. /// - Parameters:
  230. /// - treatmentObjectID: The ID of the entry to update
  231. /// - newCarbs: The new carbs value
  232. /// - newFat: The new fat value
  233. /// - newProtein: The new protein value
  234. /// - newNote: The new note text
  235. func updateEntry(
  236. _ treatmentObjectID: NSManagedObjectID,
  237. newCarbs: Decimal,
  238. newFat: Decimal,
  239. newProtein: Decimal,
  240. newNote: String
  241. ) {
  242. Task {
  243. // Get original date from entry to re-create the entry later with the updated values and the same date
  244. guard let originalEntry = await getOriginalEntryValues(treatmentObjectID) else { return }
  245. // Deletion logic for carb and FPU entries
  246. await deleteOldEntries(
  247. treatmentObjectID,
  248. originalEntry: originalEntry,
  249. newCarbs: newCarbs,
  250. newFat: newFat,
  251. newProtein: newProtein,
  252. newNote: newNote
  253. )
  254. await createNewEntries(
  255. originalDate: originalEntry.entryValues?.date ?? Date(),
  256. // TODO: should we add this to the guard or is nullish coalesce safe enough?
  257. newCarbs: newCarbs,
  258. newFat: newFat,
  259. newProtein: newProtein,
  260. newNote: newNote
  261. )
  262. await syncWithServices()
  263. }
  264. }
  265. private func createNewEntries(
  266. originalDate: Date,
  267. newCarbs: Decimal,
  268. newFat: Decimal,
  269. newProtein: Decimal,
  270. newNote: String
  271. ) async {
  272. let newEntry = CarbsEntry(
  273. id: UUID().uuidString,
  274. createdAt: Date(),
  275. actualDate: originalDate,
  276. carbs: newCarbs,
  277. fat: newFat,
  278. protein: newProtein,
  279. note: newNote,
  280. enteredBy: CarbsEntry.local,
  281. isFPU: false,
  282. fpuID: newFat > 0 || newProtein > 0 ? UUID().uuidString : nil
  283. )
  284. // Handles internally whether to create fake carbs or not based on whether fat > 0 or protein > 0
  285. await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
  286. }
  287. /// Deletes the old carb/ FPU entries and creates new ones with updated values
  288. /// - Parameters:
  289. /// - treatmentObjectID: The ID of the entry to delete
  290. /// - originalDate: The original date to preserve
  291. /// - newCarbs: The new carbs value
  292. /// - newFat: The new fat value
  293. /// - newProtein: The new protein value
  294. /// - newNote: The new note text
  295. private func deleteOldEntries(
  296. _ treatmentObjectID: NSManagedObjectID,
  297. originalEntry: (
  298. entryValues: (date: Date, oldCarbs: Double, oldFat: Double, oldProtein: Double)?,
  299. entryId: NSManagedObjectID
  300. ),
  301. newCarbs _: Decimal,
  302. newFat _: Decimal,
  303. newProtein _: Decimal,
  304. newNote _: String
  305. ) async {
  306. // TODO: cleanup
  307. // TODO: maybe pass originalEntry instead of treatmentObjectId down?
  308. if ((originalEntry.entryValues?.oldCarbs ?? 0) == 0 && (originalEntry.entryValues?.oldFat ?? 0) > 0) ||
  309. ((originalEntry.entryValues?.oldCarbs ?? 0) == 0 && (originalEntry.entryValues?.oldProtein ?? 0) > 0)
  310. {
  311. // Delete the zero-carb-entry and all its carb equivalents connected by the same fpuID from remote services and Core Data
  312. // Use fpuID
  313. await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
  314. } else if ((originalEntry.entryValues?.oldCarbs ?? 0) > 0 && (originalEntry.entryValues?.oldFat ?? 0) > 0) ||
  315. ((originalEntry.entryValues?.oldCarbs ?? 0) > 0 && (originalEntry.entryValues?.oldProtein ?? 0) > 0)
  316. {
  317. // Delete carb entry and carb equivalents that are all connected by the same fpuID from remote services and Core Data
  318. // Use fpuID
  319. await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
  320. } else {
  321. // Delete just the carb entry since there are no carb equivalents
  322. // Use NSManagedObjectID
  323. await deleteCarbs(treatmentObjectID)
  324. }
  325. }
  326. /// Retrieves the original entry values
  327. /// - Parameter objectID: The ID of the entry
  328. /// - Returns: A tuple of the old entry values and its original date and the objectID or nil
  329. private func getOriginalEntryValues(_ objectID: NSManagedObjectID) async
  330. -> (entryValues: (date: Date, oldCarbs: Double, oldFat: Double, oldProtein: Double)?, entryId: NSManagedObjectID)?
  331. {
  332. let context = CoreDataStack.shared.newTaskContext()
  333. context.name = "updateContext"
  334. context.transactionAuthor = "updateEntry"
  335. // TODO: possibly extend this by id and fpuID to not having to fetch again later on
  336. return await context.perform {
  337. do {
  338. guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored, let entryDate = entry.date
  339. else { return nil }
  340. return (
  341. entryValues: (date: entryDate, oldCarbs: entry.carbs, oldFat: entry.fat, oldProtein: entry.protein),
  342. entryId: entry.objectID
  343. )
  344. } catch let error as NSError {
  345. debugPrint("\(DebuggingIdentifiers.failed) Failed to get original date with error: \(error.userInfo)")
  346. return nil
  347. }
  348. }
  349. }
  350. /// Synchronizes the FPU/ Carb entry with all remote services in parallel
  351. private func syncWithServices() async {
  352. async let nightscoutUpload: () = provider.nightscoutManager.uploadCarbs()
  353. async let healthKitUpload: () = provider.healthkitManager.uploadCarbs()
  354. async let tidepoolUpload: () = provider.tidepoolManager.uploadCarbs()
  355. _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
  356. }
  357. // MARK: - Entry Loading
  358. /// Loads the values of a carb or FPU entry from Core Data
  359. /// - Parameter objectID: The ID of the entry to load
  360. /// - Returns: A tuple containing the entry's values, or nil if not found
  361. func loadEntryValues(from objectID: NSManagedObjectID) async
  362. -> (carbs: Decimal, fat: Decimal, protein: Decimal, note: String)?
  363. {
  364. let context = CoreDataStack.shared.persistentContainer.viewContext
  365. return await context.perform {
  366. do {
  367. guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored else { return nil }
  368. return (
  369. carbs: Decimal(entry.carbs),
  370. fat: Decimal(entry.fat),
  371. protein: Decimal(entry.protein),
  372. note: entry.note ?? ""
  373. )
  374. } catch {
  375. debugPrint("\(DebuggingIdentifiers.failed) Failed to load entry: \(error.localizedDescription)")
  376. return nil
  377. }
  378. }
  379. }
  380. // MARK: - FPU Entry Handling
  381. /// Handles the loading of FPU entries based on their type
  382. /// If the user taps on an FPU entry in the DataTable list, there are two cases:
  383. /// - the User has entered this FPU entry WITH carbs
  384. /// - the User has entered this FPU entry WITHOUT carbs
  385. /// In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
  386. /// In the second case, we need to load the zero-carb entry that actually holds the FPU values (and the carbs). For this case THIS is the entry we want to edit.
  387. /// - Parameter objectID: The ID of the FPU entry
  388. /// - Returns: A tuple containing the entry values and ID, or nil if not found
  389. func handleFPUEntry(_ objectID: NSManagedObjectID) async
  390. -> (entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String)?, entryID: NSManagedObjectID?)?
  391. {
  392. // Case 1: FPU entry WITH carbs
  393. if let correspondingCarbEntryID = await getCorrespondingCarbEntry(objectID) {
  394. if let values = await loadEntryValues(from: correspondingCarbEntryID) {
  395. return (values, correspondingCarbEntryID)
  396. }
  397. }
  398. // Case 2: FPU entry WITHOUT carbs
  399. else if let originalEntryID = await getZeroCarbNonFPUEntry(objectID) {
  400. if let values = await loadEntryValues(from: originalEntryID) {
  401. return (values, originalEntryID)
  402. }
  403. }
  404. return nil
  405. }
  406. /// Retrieves the original zero-carb non-FPU entry for a given FPU entry.
  407. /// This is used when the user has entered a FPU entry WITHOUT carbs.
  408. /// - Parameter treatmentObjectID: The ID of the FPU entry
  409. /// - Returns: The ID of the original entry, or nil if not found
  410. func getZeroCarbNonFPUEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
  411. let context = CoreDataStack.shared.newTaskContext()
  412. context.name = "fpuContext"
  413. return await context.perform {
  414. do {
  415. // Get the fpuID from the selected entry
  416. guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
  417. let fpuID = selectedEntry.fpuID
  418. else { return nil }
  419. // Fetch the original zero-carb entry (non-FPU) with the same fpuID
  420. let last24Hours = Date().addingTimeInterval(-60 * 60 * 24)
  421. let request = CarbEntryStored.fetchRequest()
  422. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
  423. NSPredicate(format: "date >= %@", last24Hours as NSDate),
  424. NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
  425. NSPredicate(format: "isFPU == NO"),
  426. NSPredicate(format: "carbs == 0")
  427. ])
  428. request.fetchLimit = 1
  429. let originalEntry = try context.fetch(request).first
  430. debugPrint("FPU fetch result: \(originalEntry != nil ? "Entry found" : "No entry found")")
  431. return originalEntry?.objectID
  432. } catch let error as NSError {
  433. debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch original FPU entry: \(error.userInfo)")
  434. return nil
  435. }
  436. }
  437. }
  438. /// Retrieves the corresponding carb entry for a given FPU entry.
  439. /// This is used when the user has entered a carb entry WITH FPUs all at once.
  440. /// - Parameter treatmentObjectID: The ID of the FPU entry
  441. /// - Returns: The ID of the corresponding carb entry, or nil if not found
  442. func getCorrespondingCarbEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
  443. let context = CoreDataStack.shared.newTaskContext()
  444. context.name = "carbContext"
  445. return await context.perform {
  446. do {
  447. // Get the fpuID from the selected entry
  448. guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
  449. let fpuID = selectedEntry.fpuID
  450. else { return nil }
  451. // Fetch the corresponding carb entry with the same fpuID
  452. let last24Hours = Date().addingTimeInterval(-24.hours.timeInterval)
  453. let request = CarbEntryStored.fetchRequest()
  454. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
  455. NSPredicate(format: "date >= %@", last24Hours as NSDate),
  456. NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
  457. NSPredicate(format: "isFPU == NO"),
  458. NSPredicate(format: "(carbs > 0) OR (fat > 0) OR (protein > 0)")
  459. ])
  460. request.fetchLimit = 1
  461. let correspondingCarbEntry = try context.fetch(request).first
  462. debugPrint(
  463. "Corresponding carb entry fetch result: \(correspondingCarbEntry != nil ? "Entry found" : "No entry found")"
  464. )
  465. return correspondingCarbEntry?.objectID
  466. } catch let error as NSError {
  467. debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch corresponding carb entry: \(error.userInfo)")
  468. return nil
  469. }
  470. }
  471. }
  472. }
  473. }
  474. extension DataTable.StateModel: DeterminationObserver, SettingsObserver {
  475. func determinationDidUpdate(_: Determination) {
  476. DispatchQueue.main.async {
  477. self.waitForSuggestion = false
  478. }
  479. }
  480. func settingsDidChange(_: FreeAPSSettings) {
  481. units = settingsManager.settings.units
  482. }
  483. }