DataTableStateModel.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  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, date: Date)?,
  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. /// - newDate: The new date for the entry
  236. func updateEntry(
  237. _ treatmentObjectID: NSManagedObjectID,
  238. newCarbs: Decimal,
  239. newFat: Decimal,
  240. newProtein: Decimal,
  241. newNote: String,
  242. newDate: Date
  243. ) {
  244. Task {
  245. // Get original date from entry to re-create the entry later with the updated values and the same date
  246. guard let originalEntry = await getOriginalEntryValues(treatmentObjectID) else { return }
  247. // Deletion logic for carb and FPU entries
  248. await deleteOldEntries(
  249. treatmentObjectID,
  250. originalEntry: originalEntry,
  251. newCarbs: newCarbs,
  252. newFat: newFat,
  253. newProtein: newProtein,
  254. newNote: newNote
  255. )
  256. await createNewEntries(
  257. originalDate: newDate,
  258. newCarbs: newCarbs,
  259. newFat: newFat,
  260. newProtein: newProtein,
  261. newNote: newNote
  262. )
  263. await syncWithServices()
  264. // Perform a determine basal sync to update cob
  265. await apsManager.determineBasalSync()
  266. }
  267. }
  268. private func createNewEntries(
  269. originalDate: Date,
  270. newCarbs: Decimal,
  271. newFat: Decimal,
  272. newProtein: Decimal,
  273. newNote: String
  274. ) async {
  275. let newEntry = CarbsEntry(
  276. id: UUID().uuidString,
  277. createdAt: Date(),
  278. actualDate: originalDate,
  279. carbs: newCarbs,
  280. fat: newFat,
  281. protein: newProtein,
  282. note: newNote,
  283. enteredBy: CarbsEntry.local,
  284. isFPU: false,
  285. fpuID: newFat > 0 || newProtein > 0 ? UUID().uuidString : nil
  286. )
  287. // Handles internally whether to create fake carbs or not based on whether fat > 0 or protein > 0
  288. await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
  289. }
  290. /// Deletes the old carb/ FPU entries and creates new ones with updated values
  291. /// - Parameters:
  292. /// - treatmentObjectID: The ID of the entry to delete
  293. /// - originalDate: The original date to preserve
  294. /// - newCarbs: The new carbs value
  295. /// - newFat: The new fat value
  296. /// - newProtein: The new protein value
  297. /// - newNote: The new note text
  298. private func deleteOldEntries(
  299. _ treatmentObjectID: NSManagedObjectID,
  300. originalEntry: (
  301. entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?,
  302. entryId: NSManagedObjectID
  303. ),
  304. newCarbs _: Decimal,
  305. newFat _: Decimal,
  306. newProtein _: Decimal,
  307. newNote _: String
  308. ) async {
  309. if ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
  310. ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
  311. {
  312. // Delete the zero-carb-entry and all its carb equivalents connected by the same fpuID from remote services and Core Data
  313. // Use fpuID
  314. await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
  315. } else if ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
  316. ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
  317. {
  318. // Delete carb entry and carb equivalents that are all connected by the same fpuID from remote services and Core Data
  319. // Use fpuID
  320. await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
  321. } else {
  322. // Delete just the carb entry since there are no carb equivalents
  323. // Use NSManagedObjectID
  324. await deleteCarbs(treatmentObjectID)
  325. }
  326. }
  327. /// Retrieves the original entry values
  328. /// - Parameter objectID: The ID of the entry
  329. /// - Returns: A tuple of the old entry values and its original date and the objectID or nil
  330. private func getOriginalEntryValues(_ objectID: NSManagedObjectID) async
  331. -> (entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?, entryId: NSManagedObjectID)?
  332. {
  333. let context = CoreDataStack.shared.newTaskContext()
  334. context.name = "updateContext"
  335. context.transactionAuthor = "updateEntry"
  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, carbs: entry.carbs, fat: entry.fat, protein: 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, date: Date)?
  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,
  368. let entryDate = entry.date
  369. else { return nil }
  370. return (
  371. carbs: Decimal(entry.carbs),
  372. fat: Decimal(entry.fat),
  373. protein: Decimal(entry.protein),
  374. note: entry.note ?? "",
  375. date: entryDate
  376. )
  377. } catch {
  378. debugPrint("\(DebuggingIdentifiers.failed) Failed to load entry: \(error.localizedDescription)")
  379. return nil
  380. }
  381. }
  382. }
  383. // MARK: - FPU Entry Handling
  384. /// Handles the loading of FPU entries based on their type
  385. /// If the user taps on an FPU entry in the DataTable list, there are two cases:
  386. /// - the User has entered this FPU entry WITH carbs
  387. /// - the User has entered this FPU entry WITHOUT carbs
  388. /// In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
  389. /// 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.
  390. /// - Parameter objectID: The ID of the FPU entry
  391. /// - Returns: A tuple containing the entry values and ID, or nil if not found
  392. func handleFPUEntry(_ objectID: NSManagedObjectID) async
  393. -> (
  394. entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?,
  395. entryID: NSManagedObjectID?
  396. )?
  397. {
  398. // Case 1: FPU entry WITH carbs
  399. if let correspondingCarbEntryID = await getCorrespondingCarbEntry(objectID) {
  400. if let values = await loadEntryValues(from: correspondingCarbEntryID) {
  401. return (values, correspondingCarbEntryID)
  402. }
  403. }
  404. // Case 2: FPU entry WITHOUT carbs
  405. else if let originalEntryID = await getZeroCarbNonFPUEntry(objectID) {
  406. if let values = await loadEntryValues(from: originalEntryID) {
  407. return (values, originalEntryID)
  408. }
  409. }
  410. return nil
  411. }
  412. /// Retrieves the original zero-carb non-FPU entry for a given FPU entry.
  413. /// This is used when the user has entered a FPU entry WITHOUT carbs.
  414. /// - Parameter treatmentObjectID: The ID of the FPU entry
  415. /// - Returns: The ID of the original entry, or nil if not found
  416. func getZeroCarbNonFPUEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
  417. let context = CoreDataStack.shared.newTaskContext()
  418. context.name = "fpuContext"
  419. return await context.perform {
  420. do {
  421. // Get the fpuID from the selected entry
  422. guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
  423. let fpuID = selectedEntry.fpuID
  424. else { return nil }
  425. // Fetch the original zero-carb entry (non-FPU) with the same fpuID
  426. let last24Hours = Date().addingTimeInterval(-60 * 60 * 24)
  427. let request = CarbEntryStored.fetchRequest()
  428. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
  429. NSPredicate(format: "date >= %@", last24Hours as NSDate),
  430. NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
  431. NSPredicate(format: "isFPU == NO"),
  432. NSPredicate(format: "carbs == 0")
  433. ])
  434. request.fetchLimit = 1
  435. let originalEntry = try context.fetch(request).first
  436. debugPrint("FPU fetch result: \(originalEntry != nil ? "Entry found" : "No entry found")")
  437. return originalEntry?.objectID
  438. } catch let error as NSError {
  439. debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch original FPU entry: \(error.userInfo)")
  440. return nil
  441. }
  442. }
  443. }
  444. /// Retrieves the corresponding carb entry for a given FPU entry.
  445. /// This is used when the user has entered a carb entry WITH FPUs all at once.
  446. /// - Parameter treatmentObjectID: The ID of the FPU entry
  447. /// - Returns: The ID of the corresponding carb entry, or nil if not found
  448. func getCorrespondingCarbEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
  449. let context = CoreDataStack.shared.newTaskContext()
  450. context.name = "carbContext"
  451. return await context.perform {
  452. do {
  453. // Get the fpuID from the selected entry
  454. guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
  455. let fpuID = selectedEntry.fpuID
  456. else { return nil }
  457. // Fetch the corresponding carb entry with the same fpuID
  458. let last24Hours = Date().addingTimeInterval(-24.hours.timeInterval)
  459. let request = CarbEntryStored.fetchRequest()
  460. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
  461. NSPredicate(format: "date >= %@", last24Hours as NSDate),
  462. NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
  463. NSPredicate(format: "isFPU == NO"),
  464. NSPredicate(format: "(carbs > 0) OR (fat > 0) OR (protein > 0)")
  465. ])
  466. request.fetchLimit = 1
  467. let correspondingCarbEntry = try context.fetch(request).first
  468. debugPrint(
  469. "Corresponding carb entry fetch result: \(correspondingCarbEntry != nil ? "Entry found" : "No entry found")"
  470. )
  471. return correspondingCarbEntry?.objectID
  472. } catch let error as NSError {
  473. debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch corresponding carb entry: \(error.userInfo)")
  474. return nil
  475. }
  476. }
  477. }
  478. }
  479. }
  480. extension DataTable.StateModel: DeterminationObserver, SettingsObserver {
  481. func determinationDidUpdate(_: Determination) {
  482. DispatchQueue.main.async {
  483. self.waitForSuggestion = false
  484. }
  485. }
  486. func settingsDidChange(_: TrioSettings) {
  487. units = settingsManager.settings.units
  488. }
  489. }