BasalRateScheduleEditor.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. //
  2. // BasalRateScheduleEditor.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 4/20/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. import LoopKit
  11. public struct BasalRateScheduleEditor: View {
  12. var schedule: DailyQuantitySchedule<Double>?
  13. var supportedBasalRates: [Double]
  14. var guardrail: Guardrail<HKQuantity>
  15. var maximumScheduleEntryCount: Int
  16. var syncSchedule: PumpManager.SyncSchedule?
  17. var save: (BasalRateSchedule) -> Void
  18. let mode: SettingsPresentationMode
  19. @Environment(\.appName) private var appName
  20. /// - Precondition: `supportedBasalRates` is nonempty and sorted in ascending order.
  21. public init(
  22. schedule: BasalRateSchedule?,
  23. supportedBasalRates: [Double],
  24. maximumBasalRate: Double?,
  25. maximumScheduleEntryCount: Int,
  26. syncSchedule: PumpManager.SyncSchedule?,
  27. onSave save: @escaping (BasalRateSchedule) -> Void,
  28. mode: SettingsPresentationMode = .settings
  29. ) {
  30. self.schedule = schedule.map { schedule in
  31. DailyQuantitySchedule(
  32. unit: .internationalUnitsPerHour,
  33. dailyItems: schedule.items
  34. )!
  35. }
  36. if let maxBasal = maximumBasalRate {
  37. let partitioningIndex = supportedBasalRates.partitioningIndex(where: { $0 > maxBasal })
  38. self.supportedBasalRates = Array(supportedBasalRates[..<partitioningIndex])
  39. } else {
  40. self.supportedBasalRates = supportedBasalRates
  41. }
  42. self.guardrail = Guardrail.basalRate(supportedBasalRates: supportedBasalRates)
  43. self.maximumScheduleEntryCount = maximumScheduleEntryCount
  44. self.syncSchedule = syncSchedule
  45. self.save = save
  46. self.mode = mode
  47. self.supportedBasalRates.removeAll(where: {
  48. !self.guardrail.absoluteBounds.contains(HKQuantity(unit: .internationalUnitsPerHour, doubleValue: $0))
  49. })
  50. }
  51. public init(
  52. viewModel: TherapySettingsViewModel,
  53. didSave: (() -> Void)? = nil
  54. ) {
  55. self.init(
  56. schedule: viewModel.therapySettings.basalRateSchedule,
  57. supportedBasalRates: viewModel.pumpSupportedIncrements!()!.basalRates ,
  58. maximumBasalRate: viewModel.therapySettings.maximumBasalRatePerHour,
  59. maximumScheduleEntryCount: viewModel.pumpSupportedIncrements!()!.maximumBasalScheduleEntryCount,
  60. syncSchedule: viewModel.syncPumpSchedule?(),
  61. onSave: { [weak viewModel] newBasalRates in
  62. viewModel?.saveBasalRates(basalRates: newBasalRates)
  63. didSave?()
  64. },
  65. mode: viewModel.mode
  66. )
  67. }
  68. public var body: some View {
  69. QuantityScheduleEditor(
  70. title: Text(TherapySetting.basalRate.title),
  71. description: description,
  72. schedule: schedule,
  73. unit: .internationalUnitsPerHour,
  74. selectableValues: supportedBasalRates,
  75. guardrail: guardrail,
  76. quantitySelectionMode: .fractional,
  77. defaultFirstScheduleItemValue: guardrail.absoluteBounds.lowerBound,
  78. scheduleItemLimit: maximumScheduleEntryCount,
  79. confirmationAlertContent: confirmationAlertContent,
  80. guardrailWarning: {
  81. BasalRateGuardrailWarning(
  82. crossedThresholds: $0,
  83. isZeroUnitRateSelectable: self.supportedBasalRates.first! == 0
  84. )
  85. },
  86. onSave: savingMechanism,
  87. mode: mode,
  88. settingType: .basalRate
  89. )
  90. }
  91. private var description: Text {
  92. Text(TherapySetting.basalRate.descriptiveText(appName: appName))
  93. }
  94. private var confirmationAlertContent: AlertContent {
  95. AlertContent(
  96. title: Text(LocalizedString("Save Basal Rates?", comment: "Alert title for confirming basal rates outside the recommended range")),
  97. message: Text(TherapySetting.basalRate.guardrailSaveWarningCaption)
  98. )
  99. }
  100. private var savingMechanism: SavingMechanism<DailyQuantitySchedule<Double>> {
  101. switch mode {
  102. case .settings:
  103. return .asynchronous { quantitySchedule, completion in
  104. precondition(self.syncSchedule != nil)
  105. self.syncSchedule?(quantitySchedule.items) { result in
  106. switch result {
  107. case .success(let syncedSchedule):
  108. DispatchQueue.main.async {
  109. self.save(syncedSchedule)
  110. }
  111. completion(nil)
  112. case .failure(let error):
  113. completion(error)
  114. }
  115. }
  116. }
  117. case .acceptanceFlow:
  118. // TODO: get timezone from pump
  119. return .synchronous { quantitySchedule in
  120. let schedule = BasalRateSchedule(dailyItems: quantitySchedule.items, timeZone: .currentFixed)!
  121. self.save(schedule)
  122. }
  123. }
  124. }
  125. }
  126. private struct BasalRateGuardrailWarning: View {
  127. var crossedThresholds: [SafetyClassification.Threshold]
  128. var isZeroUnitRateSelectable: Bool
  129. var body: some View {
  130. assert(!crossedThresholds.isEmpty)
  131. let caption = self.isZeroUnitRateSelectable && crossedThresholds.allSatisfy({ $0 == .minimum })
  132. ? Text(LocalizedString("A value of 0 U/hr means you will be scheduled to receive no basal insulin.", comment: "Warning text for basal rate of 0 U/hr"))
  133. : nil
  134. return GuardrailWarning(
  135. title: crossedThresholds.count == 1 ? singularWarningTitle(for: crossedThresholds.first!) : multipleWarningTitle,
  136. thresholds: crossedThresholds,
  137. caption: caption
  138. )
  139. }
  140. private func singularWarningTitle(for threshold: SafetyClassification.Threshold) -> Text {
  141. switch threshold {
  142. case .minimum where isZeroUnitRateSelectable:
  143. return Text(LocalizedString("No Basal Insulin", comment: "Title text for the zero basal rate warning"))
  144. case .minimum, .belowRecommended:
  145. return Text(LocalizedString("Low Basal Rate", comment: "Title text for the low basal rate warning"))
  146. case .aboveRecommended, .maximum:
  147. return Text(LocalizedString("High Basal Rate", comment: "Title text for the high basal rate warning"))
  148. }
  149. }
  150. private var multipleWarningTitle: Text {
  151. isZeroUnitRateSelectable && crossedThresholds.allSatisfy({ $0 == .minimum })
  152. ? Text(LocalizedString("No Basal Insulin", comment: "Title text for the zero basal rate warning"))
  153. : Text(LocalizedString("Basal Rates", comment: "Title text for multi-value basal rate warning"))
  154. }
  155. }