QuantityScheduleEditor.swift 8.8 KB

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