DataTableStateModel.swift 26 KB

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