StatStateModel.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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 loopStats: [(category: String, count: Int, percentage: Double)] = []
  47. var groupedLoopStats: [LoopStatsByPeriod] = []
  48. var tddStats: [TDD] = []
  49. var bolusStats: [BolusStats] = []
  50. var hourlyStats: [HourlyStats] = []
  51. var glucoseRangeStats: [GlucoseRangeStats] = []
  52. // Cache for Meal Stats
  53. var hourlyMealStats: [MealStats] = []
  54. var dailyMealStats: [MealStats] = []
  55. var dailyAveragesCache: [Date: (carbs: Double, fat: Double, protein: Double)] = [:]
  56. // Cache for TDD Stats
  57. var hourlyTDDStats: [TDDStats] = []
  58. var dailyTDDStats: [TDDStats] = []
  59. var tddAveragesCache: [Date: Double] = [:]
  60. // Cache for Bolus Stats
  61. var hourlyBolusStats: [BolusStats] = []
  62. var dailyBolusStats: [BolusStats] = []
  63. var bolusAveragesCache: [Date: (manual: Double, smb: Double, external: Double)] = [:]
  64. // Selected Duration for Glucose Stats
  65. var selectedDurationForGlucoseStats: Duration = .Today {
  66. didSet {
  67. setupGlucoseArray(for: selectedDurationForGlucoseStats)
  68. }
  69. }
  70. // Selected Duration for Insulin Stats
  71. var selectedDurationForInsulinStats: StatsTimeInterval = .Day
  72. // Selected Duration for Meal Stats
  73. var selectedDurationForMealStats: StatsTimeInterval = .Day
  74. // Selected Duration for Loop Stats
  75. var selectedDurationForLoopStats: Duration = .Today {
  76. didSet {
  77. setupLoopStatRecords()
  78. }
  79. }
  80. // Selected Glucose Chart Type
  81. var selectedGlucoseChartType: GlucoseChartType = .percentile
  82. // Selected Insulin Chart Type
  83. var selectedInsulinChartType: InsulinChartType = .totalDailyDose
  84. // Selected Looping Chart Type
  85. var selectedLoopingChartType: LoopingChartType = .loopingPerformance
  86. // Selected Meal Chart Type
  87. var selectedMealChartType: MealChartType = .totalMeals
  88. // Fetching Contexts
  89. let context = CoreDataStack.shared.newTaskContext()
  90. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  91. let tddTaskContext = CoreDataStack.shared.newTaskContext()
  92. let loopTaskContext = CoreDataStack.shared.newTaskContext()
  93. let mealTaskContext = CoreDataStack.shared.newTaskContext()
  94. let bolusTaskContext = CoreDataStack.shared.newTaskContext()
  95. /// Defines the available time periods for duration-based statistics
  96. enum Duration: String, CaseIterable, Identifiable {
  97. /// Current day
  98. case Today
  99. /// Single day view
  100. case Day = "D"
  101. /// Week view
  102. case Week = "W"
  103. /// Month view
  104. case Month = "M"
  105. /// Three month view
  106. case Total = "3 M"
  107. var id: Self { self }
  108. }
  109. /// Defines the available time intervals for statistical analysis
  110. enum StatsTimeInterval: String, CaseIterable, Identifiable {
  111. /// Single day interval
  112. case Day = "D"
  113. /// Week interval
  114. case Week = "W"
  115. /// Month interval
  116. case Month = "M"
  117. /// Three month interval
  118. case Total = "3 M"
  119. var id: Self { self }
  120. }
  121. /// Defines the main categories of statistics available in the app
  122. enum StatisticViewType: String, CaseIterable, Identifiable {
  123. /// Glucose-related statistics including AGP and distributions
  124. case glucose
  125. /// Insulin delivery statistics including TDD and bolus distributions
  126. case insulin
  127. /// Loop performance and system status statistics
  128. case looping
  129. /// Meal-related statistics and correlations
  130. case meals
  131. var id: String { rawValue }
  132. var title: String {
  133. switch self {
  134. case .glucose: return "Glucose"
  135. case .insulin: return "Insulin"
  136. case .looping: return "Looping"
  137. case .meals: return "Meals"
  138. }
  139. }
  140. }
  141. override func subscribe() {
  142. setupGlucoseArray(for: .Today)
  143. setupTDDStats()
  144. setupBolusStats()
  145. setupLoopStatRecords()
  146. setupMealStats()
  147. highLimit = settingsManager.settings.high
  148. lowLimit = settingsManager.settings.low
  149. units = settingsManager.settings.units
  150. hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
  151. timeInRangeChartStyle = settingsManager.settings.timeInRangeChartStyle
  152. }
  153. func setupGlucoseArray(for duration: Duration) {
  154. Task {
  155. let ids = await fetchGlucose(for: duration)
  156. await updateGlucoseArray(with: ids)
  157. // Calculate hourly stats and glucose range stats asynchronously with fetched glucose IDs
  158. async let hourlyStats: () = calculateHourlyStatsForGlucoseAreaChart(from: ids)
  159. async let glucoseRangeStats: () = calculateGlucoseRangeStatsForStackedChart(from: ids)
  160. _ = await (hourlyStats, glucoseRangeStats)
  161. }
  162. }
  163. private func fetchGlucose(for duration: Duration) async -> [NSManagedObjectID] {
  164. let predicate: NSPredicate
  165. switch duration {
  166. case .Day:
  167. predicate = NSPredicate.glucoseForStatsDay
  168. case .Week:
  169. predicate = NSPredicate.glucoseForStatsWeek
  170. case .Today:
  171. predicate = NSPredicate.glucoseForStatsToday
  172. case .Month:
  173. predicate = NSPredicate.glucoseForStatsMonth
  174. case .Total:
  175. predicate = NSPredicate.glucoseForStatsTotal
  176. }
  177. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  178. ofType: GlucoseStored.self,
  179. onContext: context,
  180. predicate: predicate,
  181. key: "date",
  182. ascending: false,
  183. batchSize: 100,
  184. propertiesToFetch: ["glucose", "objectID"]
  185. )
  186. return await context.perform {
  187. guard let fetchedResults = results as? [[String: Any]] else { return [] }
  188. return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
  189. }
  190. }
  191. @MainActor private func updateGlucoseArray(with IDs: [NSManagedObjectID]) {
  192. do {
  193. let glucoseObjects = try IDs.compactMap { id in
  194. try viewContext.existingObject(with: id) as? GlucoseStored
  195. }
  196. glucoseFromPersistence = glucoseObjects
  197. } catch {
  198. debugPrint(
  199. "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
  200. )
  201. }
  202. }
  203. }
  204. @Observable final class UpdateTimer {
  205. private var workItem: DispatchWorkItem?
  206. /// Schedules a delayed update action
  207. /// - Parameter action: The closure to execute after the delay
  208. /// Cancels any previously scheduled update before scheduling a new one
  209. func scheduleUpdate(action: @escaping () -> Void) {
  210. workItem?.cancel()
  211. let newWorkItem = DispatchWorkItem {
  212. action()
  213. }
  214. workItem = newWorkItem
  215. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: newWorkItem)
  216. }
  217. }
  218. }