StatStateModel.swift 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import CoreData
  2. import Foundation
  3. import Observation
  4. import SwiftUI
  5. import Swinject
  6. extension Stat {
  7. @Observable final class StateModel: BaseStateModel<Provider> {
  8. @ObservationIgnored @Injected() var settings: SettingsManager!
  9. var highLimit: Decimal = 180
  10. var lowLimit: Decimal = 70
  11. var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
  12. var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
  13. var units: GlucoseUnits = .mgdL
  14. var glucoseFromPersistence: [GlucoseStored] = []
  15. var loopStatRecords: [LoopStatRecord] = []
  16. var groupedLoopStats: [LoopStatsByPeriod] = []
  17. var mealStats: [MealStats] = []
  18. var tddStats: [TDD] = []
  19. var selectedDurationForGlucoseStats: Duration = .Today {
  20. didSet {
  21. setupGlucoseArray(for: selectedDurationForGlucoseStats)
  22. }
  23. }
  24. var selectedDurationForInsulinStats: StatsTimeInterval = .Day
  25. var selectedDurationForLoopStats: Duration = .Today {
  26. didSet {
  27. setupLoopStatRecords()
  28. }
  29. }
  30. var selectedDurationForMealStats: Duration = .Today {
  31. didSet {
  32. setupMealStats(for: selectedDurationForMealStats)
  33. }
  34. }
  35. let context = CoreDataStack.shared.newTaskContext()
  36. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  37. let determinationFetchContext = CoreDataStack.shared.newTaskContext()
  38. let loopTaskContext = CoreDataStack.shared.newTaskContext()
  39. let mealTaskContext = CoreDataStack.shared.newTaskContext()
  40. let bolusTaskContext = CoreDataStack.shared.newTaskContext()
  41. enum Duration: String, CaseIterable, Identifiable {
  42. case Today
  43. case Day = "D"
  44. case Week = "W"
  45. case Month = "M"
  46. case Total = "3 M"
  47. var id: Self { self }
  48. }
  49. enum StatsTimeInterval: String, CaseIterable, Identifiable {
  50. case Day = "D"
  51. case Week = "W"
  52. case Month = "M"
  53. case Total = "3 M"
  54. var id: Self { self }
  55. }
  56. var hourlyStats: [HourlyStats] = []
  57. var glucoseRangeStats: [GlucoseRangeStats] = []
  58. var bolusStats: [BolusStats] = []
  59. override func subscribe() {
  60. setupGlucoseArray(for: .Today)
  61. setupTDDs()
  62. setupLoopStatRecords()
  63. setupMealStats(for: selectedDurationForMealStats)
  64. updateBolusStats()
  65. highLimit = settingsManager.settings.high
  66. lowLimit = settingsManager.settings.low
  67. units = settingsManager.settings.units
  68. hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
  69. timeInRangeChartStyle = settingsManager.settings.timeInRangeChartStyle
  70. }
  71. func setupGlucoseArray(for duration: Duration) {
  72. Task {
  73. let ids = await fetchGlucose(for: duration)
  74. await updateGlucoseArray(with: ids)
  75. // Calculate hourly stats and glucose range stats asynchronously with fetched glucose IDs
  76. async let hourlyStats: () = calculateHourlyStatsForGlucoseAreaChart(from: ids)
  77. async let glucoseRangeStats: () = calculateGlucoseRangeStatsForStackedChart(from: ids)
  78. _ = await (hourlyStats, glucoseRangeStats)
  79. }
  80. }
  81. func setupTDDs() {
  82. Task {
  83. let tddStats = await fetchAndMapDeterminations()
  84. await MainActor.run {
  85. self.tddStats = tddStats
  86. }
  87. }
  88. }
  89. private func fetchGlucose(for duration: Duration) async -> [NSManagedObjectID] {
  90. let predicate: NSPredicate
  91. switch duration {
  92. case .Day:
  93. predicate = NSPredicate.glucoseForStatsDay
  94. case .Week:
  95. predicate = NSPredicate.glucoseForStatsWeek
  96. case .Today:
  97. predicate = NSPredicate.glucoseForStatsToday
  98. case .Month:
  99. predicate = NSPredicate.glucoseForStatsMonth
  100. case .Total:
  101. predicate = NSPredicate.glucoseForStatsTotal
  102. }
  103. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  104. ofType: GlucoseStored.self,
  105. onContext: context,
  106. predicate: predicate,
  107. key: "date",
  108. ascending: false,
  109. batchSize: 100,
  110. propertiesToFetch: ["glucose", "objectID"]
  111. )
  112. return await context.perform {
  113. guard let fetchedResults = results as? [[String: Any]] else { return [] }
  114. return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
  115. }
  116. }
  117. @MainActor private func updateGlucoseArray(with IDs: [NSManagedObjectID]) {
  118. do {
  119. let glucoseObjects = try IDs.compactMap { id in
  120. try viewContext.existingObject(with: id) as? GlucoseStored
  121. }
  122. glucoseFromPersistence = glucoseObjects
  123. } catch {
  124. debugPrint(
  125. "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the glucose array: \(error.localizedDescription)"
  126. )
  127. }
  128. }
  129. var averageTDD: Decimal {
  130. let calendar = Calendar.current
  131. let now = Date()
  132. // Filter TDDs based on selected time frame
  133. let filteredTDDs: [TDD] = tddStats.filter { tdd in
  134. guard let timestamp = tdd.timestamp else { return false }
  135. switch selectedDurationForInsulinStats {
  136. case .Day:
  137. // Last 3 days
  138. let threeDaysAgo = calendar.date(byAdding: .day, value: -3, to: now)!
  139. return timestamp >= threeDaysAgo
  140. case .Week:
  141. // Last week
  142. let weekAgo = calendar.date(byAdding: .weekOfYear, value: -1, to: now)!
  143. return timestamp >= weekAgo
  144. case .Month:
  145. // Last month
  146. let monthAgo = calendar.date(byAdding: .month, value: -1, to: now)!
  147. return timestamp >= monthAgo
  148. case .Total:
  149. // Last 3 months
  150. let threeMonthsAgo = calendar.date(byAdding: .month, value: -3, to: now)!
  151. return timestamp >= threeMonthsAgo
  152. }
  153. }
  154. let sum = filteredTDDs.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
  155. return filteredTDDs.isEmpty ? 0 : sum / Decimal(filteredTDDs.count)
  156. }
  157. func calculateAverageTDD(from startDate: Date, to endDate: Date) -> Decimal {
  158. let filteredTDDs = tddStats.filter { tdd in
  159. guard let timestamp = tdd.timestamp else { return false }
  160. return timestamp >= startDate && timestamp <= endDate
  161. }
  162. let sum = filteredTDDs.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
  163. return filteredTDDs.isEmpty ? 0 : sum / Decimal(filteredTDDs.count)
  164. }
  165. }
  166. }