QuantityScheduleEditor.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. //
  2. // QuantityScheduleEditor.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 4/24/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. import LoopKit
  11. struct QuantityScheduleEditor<ActionAreaContent: View>: View {
  12. enum QuantitySelectionMode {
  13. /// A single picker for selecting quantity values.
  14. case whole
  15. // A two-component picker for selecting the whole and fractional quantity values independently.
  16. case fractional
  17. }
  18. @Environment(\.guidanceColors) var guidanceColors
  19. var title: Text
  20. var description: Text
  21. var initialScheduleItems: [RepeatingScheduleValue<Double>]
  22. @State var scheduleItems: [RepeatingScheduleValue<Double>]
  23. var unit: HKUnit
  24. var selectableValues: [Double]
  25. var quantitySelectionMode: QuantitySelectionMode
  26. var guardrail: Guardrail<HKQuantity>
  27. var defaultFirstScheduleItemValue: HKQuantity
  28. var scheduleItemLimit: Int
  29. var confirmationAlertContent: AlertContent
  30. var guardrailWarning: (_ crossedThresholds: [SafetyClassification.Threshold]) -> ActionAreaContent
  31. var savingMechanism: SavingMechanism<DailyQuantitySchedule<Double>>
  32. var mode: SettingsPresentationMode
  33. var settingType: TherapySetting
  34. @State private var userDidTap: Bool = false
  35. var body: some View {
  36. ScheduleEditor(
  37. title: title,
  38. description: description,
  39. scheduleItems: $scheduleItems,
  40. initialScheduleItems: initialScheduleItems,
  41. defaultFirstScheduleItemValue: defaultFirstScheduleItemValue.doubleValue(for: unit),
  42. scheduleItemLimit: scheduleItemLimit,
  43. saveConfirmation: saveConfirmation,
  44. valueContent: { value, isEditing in
  45. GuardrailConstrainedQuantityView(
  46. value: HKQuantity(unit: self.unit, doubleValue: value),
  47. unit: self.unit,
  48. guardrail: self.guardrail,
  49. isEditing: isEditing
  50. )
  51. },
  52. valuePicker: { item, availableWidth in
  53. if self.quantitySelectionMode == .whole {
  54. QuantityPicker(
  55. value: item.value.animation().withUnit(self.unit),
  56. unit: self.unit,
  57. guardrail: self.guardrail,
  58. selectableValues: self.selectableValues,
  59. guidanceColors: self.guidanceColors
  60. )
  61. .frame(width: availableWidth / 2)
  62. // Ensure overlaid unit label is not clipped
  63. .padding(.trailing, self.unitLabelWidth + self.unitLabelSpacing)
  64. .clipped()
  65. } else {
  66. FractionalQuantityPicker(
  67. value: item.value.animation().withUnit(self.unit),
  68. unit: self.unit,
  69. guardrail: self.guardrail,
  70. selectableValues: self.selectableValues,
  71. usageContext: .component(availableWidth: availableWidth)
  72. )
  73. }
  74. },
  75. actionAreaContent: {
  76. instructionalContentIfNecessary
  77. guardrailWarningIfNecessary
  78. },
  79. savingMechanism: savingMechanism.pullback { items in
  80. DailyQuantitySchedule(unit: self.unit, dailyItems: items)!
  81. },
  82. mode: mode,
  83. therapySettingType: settingType
  84. )
  85. .onTapGesture {
  86. self.userDidTap = true
  87. }
  88. }
  89. private var saveConfirmation: SaveConfirmation {
  90. crossedThresholds.isEmpty ? .notRequired : .required(confirmationAlertContent)
  91. }
  92. private var unitLabelWidth: CGFloat {
  93. let attributedUnitString = NSAttributedString(
  94. string: unit.shortLocalizedUnitString(),
  95. attributes: [.font: UIFont.preferredFont(forTextStyle: .body)]
  96. )
  97. return attributedUnitString.size().width
  98. }
  99. private var unitLabelSpacing: CGFloat { 8 }
  100. private var instructionalContentIfNecessary: some View {
  101. return Group {
  102. if mode == .acceptanceFlow && !userDidTap {
  103. instructionalContent
  104. }
  105. }
  106. }
  107. private var instructionalContent: some View {
  108. HStack { // to align with guardrail warning, if present
  109. VStack (alignment: .leading, spacing: 20) {
  110. Text(LocalizedString("You can edit a setting by tapping into any line item.", comment: "Description of how to edit setting"))
  111. Text(LocalizedString("You can add entries for different times of day by using the ➕.", comment: "Description of how to add a range"))
  112. }
  113. .foregroundColor(.secondary)
  114. .font(.subheadline)
  115. Spacer()
  116. }
  117. }
  118. private var guardrailWarningIfNecessary: some View {
  119. let crossedThresholds = self.crossedThresholds
  120. return Group {
  121. if !crossedThresholds.isEmpty && (userDidTap || mode == .settings) {
  122. guardrailWarning(crossedThresholds)
  123. }
  124. }
  125. }
  126. private var crossedThresholds: [SafetyClassification.Threshold] {
  127. scheduleItems.lazy
  128. .map { HKQuantity(unit: self.unit, doubleValue: $0.value) }
  129. .compactMap { quantity in
  130. switch guardrail.classification(for: quantity) {
  131. case .withinRecommendedRange:
  132. return nil
  133. case .outsideRecommendedRange(let threshold):
  134. return threshold
  135. }
  136. }
  137. }
  138. }
  139. // MARK: - Initializers
  140. extension QuantityScheduleEditor {
  141. init(
  142. title: Text,
  143. description: Text,
  144. schedule: DailyQuantitySchedule<Double>?,
  145. unit: HKUnit,
  146. selectableValues: [Double],
  147. guardrail: Guardrail<HKQuantity>,
  148. quantitySelectionMode: QuantitySelectionMode = .whole,
  149. defaultFirstScheduleItemValue: HKQuantity,
  150. scheduleItemLimit: Int = 48,
  151. confirmationAlertContent: AlertContent,
  152. @ViewBuilder guardrailWarning: @escaping (_ thresholds: [SafetyClassification.Threshold]) -> ActionAreaContent,
  153. onSave savingMechanism: SavingMechanism<DailyQuantitySchedule<Double>>,
  154. mode: SettingsPresentationMode = .settings,
  155. settingType: TherapySetting = .none
  156. ) {
  157. self.title = title
  158. self.description = description
  159. self.initialScheduleItems = schedule?.items ?? []
  160. self._scheduleItems = State(initialValue: schedule?.items ?? [])
  161. self.unit = unit
  162. self.quantitySelectionMode = quantitySelectionMode
  163. self.selectableValues = selectableValues
  164. self.guardrail = guardrail
  165. self.defaultFirstScheduleItemValue = defaultFirstScheduleItemValue
  166. self.scheduleItemLimit = scheduleItemLimit
  167. self.confirmationAlertContent = confirmationAlertContent
  168. self.guardrailWarning = guardrailWarning
  169. self.savingMechanism = savingMechanism
  170. self.mode = mode
  171. self.settingType = settingType
  172. }
  173. init(
  174. title: Text,
  175. description: Text,
  176. schedule: DailyQuantitySchedule<Double>?,
  177. unit: HKUnit,
  178. guardrail: Guardrail<HKQuantity>,
  179. selectableValueStride: HKQuantity,
  180. quantitySelectionMode: QuantitySelectionMode = .whole,
  181. defaultFirstScheduleItemValue: HKQuantity,
  182. scheduleItemLimit: Int = 48,
  183. confirmationAlertContent: AlertContent,
  184. @ViewBuilder guardrailWarning: @escaping (_ thresholds: [SafetyClassification.Threshold]) -> ActionAreaContent,
  185. onSave save: @escaping (DailyQuantitySchedule<Double>) -> Void,
  186. mode: SettingsPresentationMode = .settings,
  187. settingType: TherapySetting = .none
  188. ) {
  189. let selectableValues = guardrail.allValues(stridingBy: selectableValueStride, unit: unit)
  190. self.init(
  191. title: title,
  192. description: description,
  193. schedule: schedule,
  194. unit: unit,
  195. selectableValues: selectableValues,
  196. guardrail: guardrail,
  197. quantitySelectionMode: quantitySelectionMode,
  198. defaultFirstScheduleItemValue: defaultFirstScheduleItemValue,
  199. scheduleItemLimit: scheduleItemLimit,
  200. confirmationAlertContent: confirmationAlertContent,
  201. guardrailWarning: guardrailWarning,
  202. onSave: .synchronous(save),
  203. mode: mode,
  204. settingType: settingType
  205. )
  206. }
  207. }