DataManager.swift 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import CoreData
  2. import Foundation
  3. // Fetch Data for Glucose and Determination from Core Data and map them to the Structs in order to pass them thread safe to the glucoseDidUpdate/ pushUpdate function
  4. @available(iOS 16.2, *)
  5. extension LiveActivityManager {
  6. func fetchAndMapGlucose() async throws -> [GlucoseData] {
  7. let context = CoreDataStack.shared.newTaskContext()
  8. context.name = "fetchAndMapGlucose"
  9. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  10. ofType: GlucoseStored.self,
  11. onContext: context,
  12. predicate: NSPredicate.predicateForSixHoursAgo,
  13. key: "date",
  14. ascending: false
  15. )
  16. return try await context.perform {
  17. guard let glucoseResults = results as? [GlucoseStored] else {
  18. throw CoreDataError.fetchError(function: #function, file: #file)
  19. }
  20. return glucoseResults.map {
  21. GlucoseData(glucose: Int($0.glucose), date: $0.date ?? Date(), direction: $0.directionEnum)
  22. }
  23. }
  24. }
  25. // TODO: extract logic or at least rename function appropiately
  26. func fetchAndMapDetermination() async throws -> DeterminationData? {
  27. let context = CoreDataStack.shared.newTaskContext()
  28. context.name = "fetchAndMapDetermination"
  29. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  30. ofType: OrefDetermination.self,
  31. onContext: context,
  32. predicate: NSPredicate.predicateFor30MinAgoForDetermination,
  33. key: "deliverAt",
  34. ascending: false,
  35. fetchLimit: 1,
  36. relationshipKeyPathsForPrefetching: ["forecasts", "forecasts.forecastValues"]
  37. )
  38. let tddResults = try await CoreDataStack.shared.fetchEntitiesAsync(
  39. ofType: TDDStored.self,
  40. onContext: context,
  41. predicate: NSPredicate.predicateFor30MinAgo,
  42. key: "date",
  43. ascending: false,
  44. fetchLimit: 1,
  45. propertiesToFetch: ["total"]
  46. )
  47. return try await context.perform {
  48. guard let determinationResults = results as? [OrefDetermination],
  49. let tddResults = tddResults as? [[String: Any]]
  50. else {
  51. throw CoreDataError.fetchError(function: #function, file: #file)
  52. }
  53. guard let determination = determinationResults.first else {
  54. return nil
  55. }
  56. let tddValue = (tddResults.first?["total"] as? NSDecimalNumber)?.decimalValue ?? 0
  57. // Compute cone bounds and per-type lines from forecast relationships (cap at 24 values = 2h)
  58. var allForecastValues = [[Int]]()
  59. var forecastLines = [(type: String, values: [Int])]()
  60. if let forecasts = determination.forecasts {
  61. let hasCarbs = forecasts.contains(where: {
  62. ($0.type == "cob" || $0.type == "uam") && !$0.forecastValuesArray.isEmpty
  63. })
  64. for forecast in forecasts.sorted(by: { ($0.type ?? "") < ($1.type ?? "") }) {
  65. let values = forecast.forecastValuesArray.prefix(24).map { Int($0.value) }
  66. guard !values.isEmpty else { continue }
  67. // iob is hidden when cob or uam are active (matches phone app behavior)
  68. if forecast.type == "iob", hasCarbs { continue }
  69. allForecastValues.append(Array(values))
  70. if let type = forecast.type {
  71. forecastLines.append((type: type, values: Array(values)))
  72. }
  73. }
  74. }
  75. let minCount = allForecastValues.map(\.count).min() ?? 0
  76. var minForecast = [Int]()
  77. var maxForecast = [Int]()
  78. for index in 0 ..< minCount {
  79. let col = allForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }
  80. minForecast.append(col.min() ?? 0)
  81. maxForecast.append(col.max() ?? 0)
  82. }
  83. return DeterminationData(
  84. cob: Int(determination.cob),
  85. tdd: tddValue,
  86. target: determination.currentTarget?.decimalValue ?? 0,
  87. date: determination.deliverAt,
  88. minForecast: minForecast,
  89. maxForecast: maxForecast,
  90. forecastLines: forecastLines
  91. )
  92. }
  93. }
  94. func fetchAndMapTempTarget() async throws -> TempTargetData? {
  95. try await fetchAndMapLatest(
  96. ofType: TempTargetStored.self,
  97. predicate: .predicateForOneDayAgo,
  98. key: "date",
  99. propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
  100. ) { row in
  101. TempTargetData(
  102. isActive: row["enabled"] as? Bool ?? false,
  103. tempTargetName: row["name"] as? String ?? "Temp Target",
  104. date: row["date"] as? Date ?? Date(),
  105. duration: row["duration"] as? Decimal ?? 0,
  106. target: row["target"] as? Decimal ?? 0
  107. )
  108. }
  109. }
  110. func fetchAndMapOverride() async throws -> OverrideData? {
  111. try await fetchAndMapLatest(
  112. ofType: OverrideStored.self,
  113. predicate: .predicateForOneDayAgo,
  114. key: "date",
  115. propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
  116. ) { row in
  117. OverrideData(
  118. isActive: row["enabled"] as? Bool ?? false,
  119. overrideName: row["name"] as? String ?? "Override",
  120. date: row["date"] as? Date ?? Date(),
  121. duration: row["duration"] as? Decimal ?? 0,
  122. target: row["target"] as? Decimal ?? 0
  123. )
  124. }
  125. }
  126. private func fetchAndMapLatest<Entity: NSManagedObject, Output>(
  127. ofType type: Entity.Type,
  128. predicate: NSPredicate,
  129. key: String,
  130. propertiesToFetch: [String],
  131. map: @escaping ([String: Any]) -> Output
  132. ) async throws -> Output? {
  133. let context = CoreDataStack.shared.newTaskContext()
  134. context.name = "fetchAndMapLatest"
  135. let results = try await CoreDataStack.shared.fetchEntitiesAsync(
  136. ofType: type,
  137. onContext: context,
  138. predicate: predicate,
  139. key: key,
  140. ascending: false,
  141. fetchLimit: 1,
  142. propertiesToFetch: propertiesToFetch
  143. )
  144. return try await context.perform {
  145. guard let rows = results as? [[String: Any]] else {
  146. throw CoreDataError.fetchError(function: #function, file: #file)
  147. }
  148. return rows.first.map(map)
  149. }
  150. }
  151. }