DynamicSettingsStateModel.swift 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import Combine
  2. import CoreData
  3. import Observation
  4. import SwiftUI
  5. extension DynamicSettings {
  6. final class StateModel: BaseStateModel<Provider> {
  7. @Injected() var settings: SettingsManager!
  8. @Injected() var storage: FileStorage!
  9. @Injected() var tddStorage: TDDStorage!
  10. // this is an *interim* fix to provide better UI/UX
  11. // FIXME: needs to be refactored, once oref-swift lands and dynamicISF becomes swift-bound
  12. @Published var dynamicSensitivityType: DynamicSensitivityType = .disabled {
  13. didSet {
  14. switch dynamicSensitivityType {
  15. case .logarithmic:
  16. useNewFormula = true
  17. sigmoid = false
  18. case .sigmoid:
  19. useNewFormula = true
  20. sigmoid = true
  21. default:
  22. useNewFormula = false
  23. sigmoid = false
  24. }
  25. }
  26. }
  27. @Published var hasValidTDD: Bool = false
  28. @Published var useNewFormula: Bool = false
  29. @Published var sigmoid: Bool = false
  30. @Published var adjustmentFactor: Decimal = 0.8
  31. @Published var adjustmentFactorSigmoid: Decimal = 0.5
  32. @Published var weightPercentage: Decimal = 0.65
  33. @Published var tddAdjBasal: Bool = false
  34. @ObservedObject var pickerSettingsProvider = PickerSettingsProvider.shared
  35. var units: GlucoseUnits = .mgdL
  36. override func subscribe() {
  37. units = settingsManager.settings.units
  38. /// DynamicISF handling
  39. /// Initially, load once from storage and infer `dynamicSensitivityType` based on values of `useNewFormula` (log) and/or `sigmoid`
  40. let storedUseNewFormula = settingsManager.preferences.useNewFormula
  41. let storedSigmoid = settingsManager.preferences.sigmoid
  42. inferDynamicSensitivityType(useNewFormula: storedUseNewFormula, sigmoid: storedSigmoid)
  43. /// Subsequently, subscribe to changes from the UI and persist them in the (kept for now) two variables
  44. subscribePreferencesSetting(\.useNewFormula, on: $useNewFormula) { _ in }
  45. subscribePreferencesSetting(\.sigmoid, on: $sigmoid) { _ in }
  46. subscribePreferencesSetting(\.adjustmentFactor, on: $adjustmentFactor) { adjustmentFactor = $0 }
  47. subscribePreferencesSetting(\.adjustmentFactorSigmoid, on: $adjustmentFactorSigmoid) { adjustmentFactorSigmoid = $0 }
  48. subscribePreferencesSetting(\.weightPercentage, on: $weightPercentage) { weightPercentage = $0 }
  49. subscribePreferencesSetting(\.tddAdjBasal, on: $tddAdjBasal) { tddAdjBasal = $0 }
  50. Task {
  51. do {
  52. let hasValidTDD = try await tddStorage.hasSufficientTDD()
  53. await MainActor.run {
  54. self.hasValidTDD = hasValidTDD
  55. }
  56. } catch {
  57. debug(.coreData, "Error when fetching TDD for validity checking: \(error)")
  58. await MainActor.run {
  59. hasValidTDD = false
  60. }
  61. }
  62. }
  63. }
  64. /// Infers the `dynamicSensitivityType` based on the stored values of `useNewFormula` and `sigmoid`.
  65. /// - Logic:
  66. /// - If `useNewFormula` is `true` and `sigmoid` is `false`, sets type to `.logarithmic`.
  67. /// - If both `useNewFormula` and `sigmoid` are `true`, sets type to `.sigmoid`.
  68. /// - Otherwise, sets type to `.disabled`.
  69. ///
  70. /// This is used at startup to derive the dynamic sensitivity state from persisted values until
  71. /// a future refactor makes `dynamicSensitivityType` a first-class stored preference.
  72. // FIXME: needs to be refactored, once oref-swift lands and dynamicISF becomes swift-bound
  73. private func inferDynamicSensitivityType(useNewFormula: Bool, sigmoid: Bool) {
  74. if useNewFormula {
  75. dynamicSensitivityType = sigmoid ? .sigmoid : .logarithmic
  76. } else {
  77. dynamicSensitivityType = .disabled
  78. }
  79. }
  80. /// Checks if there is enough Total Daily Dose (TDD) data collected over the past 7 days.
  81. ///
  82. /// This function performs a count fetch for TDDStored records in Core Data where:
  83. /// - The record's date is within the last 7 days.
  84. /// - The total value is greater than 0.
  85. ///
  86. /// It then checks if at least 85% of the expected data points are present,
  87. /// assuming at least 288 expected entries per day (one every 5 minutes).
  88. ///
  89. /// - Returns: `true` if sufficient TDD data is available, otherwise `false`.
  90. /// - Throws: An error if the Core Data count operation fails.
  91. private func hasSufficientTDD() throws -> Bool {
  92. let context = CoreDataStack.shared.newTaskContext()
  93. context.name = "DynamicSettingsStateModel.hasSufficientTDD"
  94. var result = false
  95. context.performAndWait {
  96. let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "TDDStored")
  97. fetchRequest.predicate = NSPredicate(
  98. format: "date > %@ AND total > 0",
  99. Date().addingTimeInterval(-86400 * 7) as NSDate
  100. )
  101. fetchRequest.resultType = .countResultType
  102. let count = (try? context.count(for: fetchRequest)) ?? 0
  103. let threshold = Int(Double(7 * 288) * 0.85)
  104. result = count >= threshold
  105. }
  106. return result
  107. }
  108. }
  109. }
  110. extension DynamicSettings.StateModel: SettingsObserver {
  111. func settingsDidChange(_: TrioSettings) {
  112. units = settingsManager.settings.units
  113. }
  114. }
  115. extension DynamicSettings {
  116. enum DynamicSensitivityType: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
  117. var id: String { rawValue }
  118. case disabled
  119. case logarithmic
  120. case sigmoid
  121. var displayName: String {
  122. switch self {
  123. case .disabled:
  124. return String(localized: "Disabled")
  125. case .logarithmic:
  126. return String(localized: "Logarithmic")
  127. case .sigmoid:
  128. return String(localized: "Sigmoid")
  129. }
  130. }
  131. }
  132. }