QuantityScheduleEditor.swift 8.4 KB

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