StatStateModel.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import CoreData
  2. import Foundation
  3. import Observation
  4. import SwiftUI
  5. import Swinject
  6. extension Stat {
  7. /// Defines the available types of glucose charts
  8. enum GlucoseChartType: String, CaseIterable {
  9. /// Ambulatory Glucose Profile showing percentile ranges
  10. case percentile = "Percentile"
  11. /// Time-based distribution of glucose ranges
  12. case distribution = "Distribution"
  13. }
  14. /// Defines the available types of insulin charts
  15. enum InsulinChartType: String, CaseIterable {
  16. /// Shows total daily insulin doses
  17. case totalDailyDose = "Total Daily Dose"
  18. /// Shows distribution of bolus types
  19. case bolusDistribution = "Bolus Distribution"
  20. }
  21. /// Defines the available types of looping charts
  22. enum LoopingChartType: String, CaseIterable {
  23. /// Shows loop completion and success rates
  24. case loopingPerformance = "Looping Performance"
  25. /// Shows CGM connection status over time
  26. case cgmConnectionTrace = "CGM Connection Trace"
  27. /// Shows Trio pump uptime statistics
  28. case trioUpTime = "Trio Up-Time"
  29. }
  30. /// Defines the available types of meal charts
  31. enum MealChartType: String, CaseIterable {
  32. /// Shows total meal statistics
  33. case totalMeals = "Total Meals"
  34. /// Shows correlation between meals and glucose excursions
  35. case mealToHypoHyperDistribution = "Meal to Hypo/Hyper"
  36. }
  37. @Observable final class StateModel: BaseStateModel<Provider> {
  38. @ObservationIgnored @Injected() var settings: SettingsManager!
  39. var highLimit: Decimal = 180
  40. var lowLimit: Decimal = 70
  41. var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
  42. var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
  43. var units: GlucoseUnits = .mgdL
  44. var glucoseFromPersistence: [GlucoseStored] = []
  45. var loopStatRecords: [LoopStatRecord] = []
  46. var groupedLoopStats: [LoopStatsByPeriod] = []
  47. var tddStats: [TDD] = []
  48. var bolusStats: [BolusStats] = []
  49. var hourlyStats: [HourlyStats] = []
  50. var glucoseRangeStats: [GlucoseRangeStats] = []
  51. // Cache for Meal Stats
  52. var hourlyMealStats: [MealStats] = []
  53. var dailyMealStats: [MealStats] = []
  54. var dailyAveragesCache: [Date: (carbs: Double, fat: Double, protein: Double)] = [:]
  55. // Cache for TDD Stats
  56. var hourlyTDDStats: [TDDStats] = []
  57. var dailyTDDStats: [TDDStats] = []
  58. var tddAveragesCache: [Date: Double] = [:]
  59. // Cache for Bolus Stats
  60. var hourlyBolusStats: [BolusStats] = []
  61. var dailyBolusStats: [BolusStats] = []
  62. var bolusAveragesCache: [Date: (manual: Double, smb: Double, external: Double)] = [:]
  63. // Selected Duration for Glucose Stats
  64. var selectedDurationForGlucoseStats: Duration = .Today {
  65. didSet {
  66. setupGlucoseArray(for: selectedDurationForGlucoseStats)
  67. }
  68. }
  69. // Selected Duration for Insulin Stats
  70. var selectedDurationForInsulinStats: StatsTimeInterval = .Day
  71. // Selected Duration for Meal Stats
  72. var selectedDurationForMealStats: StatsTimeInterval = .Day
  73. // Selected Duration for Loop Stats
  74. var selectedDurationForLoopStats: Duration = .Today {
  75. didSet {
  76. setupLoopStatRecords()
  77. }
  78. }
  79. // Selected Glucose Chart Type
  80. var selectedGlucoseChartType: GlucoseChartType = .percentile
  81. // Selected Insulin Chart Type
  82. var selectedInsulinChartType: InsulinChartType = .totalDailyDose
  83. // Selected Looping Chart Type
  84. var selectedLoopingChartType: LoopingChartType = .loopingPerformance
  85. // Selected Meal Chart Type
  86. var selectedMealChartType: MealChartType = .totalMeals
  87. // Fetching Contexts
  88. let context = CoreDataStack.shared.newTaskContext()
  89. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  90. let tddTaskContext = CoreDataStack.shared.newTaskContext()
  91. let loopTaskContext = CoreDataStack.shared.newTaskContext()
  92. let mealTaskContext = CoreDataStack.shared.newTaskContext()
  93. let bolusTaskContext = CoreDataStack.shared.newTaskContext()
  94. /// Defines the available time periods for duration-based statistics
  95. enum Duration: String, CaseIterable, Identifiable {
  96. /// Current day
  97. case Today
  98. /// Single day view
  99. case Day = "D"
  100. /// Week view
  101. case Week = "W"
  102. /// Month view
  103. case Month = "M"
  104. /// Three month view
  105. case Total = "3 M"
  106. var id: Self { self }
  107. }
  108. /// Defines the available time intervals for statistical analysis
  109. enum StatsTimeInterval: String, CaseIterable, Identifiable {
  110. /// Single day interval
  111. case Day = "D"
  112. /// Week interval
  113. case Week = "W"
  114. /// Month interval
  115. case Month = "M"
  116. /// Three month interval
  117. case Total = "3 M"
  118. var id: Self { self }
  119. }
  120. /// Defines the main categories of statistics available in the app
  121. enum StatisticViewType: String, CaseIterable, Identifiable {
  122. /// Glucose-related statistics including AGP and distributions
  123. case glucose
  124. /// Insulin delivery statistics including TDD and bolus distributions
  125. case insulin
  126. /// Loop performance and system status statistics
  127. case looping
  128. /// Meal-related statistics and correlations
  129. case meals
  130. var id: String { rawValue }
  131. var title: String {
  132. switch self {
  133. case .glucose: return "Glucose"
  134. case .insulin: return "Insulin"
  135. case .looping: return "Looping"
  136. case .meals: return "Meals"
  137. }
  138. }
  139. }
  140. override func subscribe() {
  141. setupGlucoseArray(for: .Today)
  142. setupTDDStats()
  143. setupBolusStats()
  144. setupLoopStatRecords()
  145. setupMealStats()
  146. highLimit = settingsManager.settings.high
  147. lowLimit = settingsManager.settings.low
  148. units = settingsManager.settings.units
  149. hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
  150. timeInRangeChartStyle = settingsManager.settings.timeInRangeChartStyle
  151. }
  152. func setupGlucoseArray(for duration: Duration) {
  153. Task {
  154. let ids = await fetchGlucose(for: duration)
  155. await updateGlucoseArray(with: ids)
  156. // Calculate hourly stats and glucose range stats asynchronously with fetched glucose IDs
  157. async let hourlyStats: () = calculateHourlyStatsForGlucoseAreaChart(from: ids)
  158. async let glucoseRangeStats: () = calculateGlucoseRangeStatsForStackedChart(from: ids)
  159. _ = await (hourlyStats, glucoseRangeStats)
  160. }
  161. }
  162. private func fetchGlucose(for duration: Duration) async -> [NSManagedObjectID] {
  163. let predicate: NSPredicate
  164. switch duration {
  165. case .Day:
  166. predicate = NSPredicate.glucoseForStatsDay
  167. case .Week:
  168. predicate = NSPredicate.glucoseForStatsWeek
  169. case .Today:
  170. predicate = NSPredicate.glucoseForStatsToday
  171. case .Month:
  172. predicate = NSPredicate.glucoseForStatsMonth
  173. case .Total:
  174. predicate = NSPredicate.glucoseForStatsTotal
  175. }
  176. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  177. ofType: GlucoseStored.self,
  178. onContext: context,
  179. predicate: predicate,
  180. key: "date",
  181. ascending: false,
  182. batchSize: 100,
  183. propertiesToFetch: ["glucose", "objectID"]
  184. )
  185. return await context.perform {
  186. guard let fetchedResults = results as? [[String: Any]] else { return [] }
  187. return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
  188. }
  189. }
  190. @MainActor private func updateGlucoseArray(with IDs: [NSManagedObjectID]) {
  191. do {
  192. let glucoseObjects = try IDs.compactMap { id in
  193. try viewContext.existingObject(with: id) as? GlucoseStored
  194. }
  195. glucoseFromPersistence = glucoseObjects
  196. } catch {
  197. debugPrint(
  198. "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
  199. )
  200. }
  201. }
  202. }
  203. @Observable final class UpdateTimer {
  204. private var workItem: DispatchWorkItem?
  205. /// Schedules a delayed update action
  206. /// - Parameter action: The closure to execute after the delay
  207. /// Cancels any previously scheduled update before scheduling a new one
  208. func scheduleUpdate(action: @escaping () -> Void) {
  209. workItem?.cancel()
  210. let newWorkItem = DispatchWorkItem {
  211. action()
  212. }
  213. workItem = newWorkItem
  214. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: newWorkItem)
  215. }
  216. }
  217. }