SettingInputSection.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import SwiftUI
  2. struct SettingInputSection<VerboseHint: View>: View {
  3. enum SettingInputSectionType: Equatable {
  4. case decimal(String)
  5. case boolean
  6. case conditionalDecimal(String)
  7. static func == (lhs: SettingInputSectionType, rhs: SettingInputSectionType) -> Bool {
  8. switch (lhs, rhs) {
  9. case (.boolean, .boolean):
  10. return true
  11. case let (.decimal(lhsValue), .decimal(rhsValue)):
  12. return lhsValue == rhsValue
  13. case let (.conditionalDecimal(lhsValue), .conditionalDecimal(rhsValue)):
  14. return lhsValue == rhsValue
  15. default:
  16. return false
  17. }
  18. }
  19. }
  20. @Binding var decimalValue: Decimal
  21. @Binding var booleanValue: Bool
  22. @Binding var shouldDisplayHint: Bool
  23. @Binding var selectedVerboseHint: (any View)?
  24. var units: GlucoseUnits
  25. var type: SettingInputSectionType
  26. var label: String
  27. var conditionalLabel: String?
  28. var miniHint: String
  29. var verboseHint: VerboseHint
  30. var headerText: String?
  31. var footerText: String?
  32. var isToggleDisabled: Bool = false
  33. var miniHintColor: Color = .secondary
  34. @ObservedObject private var pickerSettingsProvider = PickerSettingsProvider.shared
  35. @State private var displayPicker: Bool = false
  36. @State private var displayConditionalPicker: Bool = false
  37. var body: some View {
  38. Section(
  39. content: {
  40. VStack {
  41. switch type {
  42. case let .decimal(key):
  43. if let setting = getPickerSetting(for: key) {
  44. pickerView(
  45. label: label,
  46. displayPicker: $displayPicker,
  47. setting: setting,
  48. decimalValue: $decimalValue
  49. )
  50. }
  51. case .boolean:
  52. toggleView(label: label, isOn: $booleanValue)
  53. .disabled(isToggleDisabled)
  54. case let .conditionalDecimal(key):
  55. VStack {
  56. toggleView(label: label, isOn: $booleanValue)
  57. if booleanValue, let setting = getPickerSetting(for: key) {
  58. pickerView(
  59. label: conditionalLabel ?? label,
  60. displayPicker: $displayConditionalPicker,
  61. setting: setting,
  62. decimalValue: $decimalValue
  63. )
  64. }
  65. }
  66. }
  67. hintSection(
  68. miniHint: miniHint,
  69. shouldDisplayHint: $shouldDisplayHint,
  70. verboseHint: verboseHint,
  71. miniHintColor: miniHintColor
  72. )
  73. }
  74. },
  75. header: { headerText.map(Text.init) },
  76. footer: { footerText.map(Text.init) }
  77. ).listRowBackground(Color.chart)
  78. }
  79. // Helper function to retrieve PickerSetting based on key
  80. private func getPickerSetting(for key: String) -> PickerSetting? {
  81. switch key {
  82. case "lowGlucose":
  83. return pickerSettingsProvider.settings.lowGlucose
  84. case "highGlucose":
  85. return pickerSettingsProvider.settings.highGlucose
  86. case "carbsRequiredThreshold":
  87. return pickerSettingsProvider.settings.carbsRequiredThreshold
  88. case "individualAdjustmentFactor":
  89. return pickerSettingsProvider.settings.individualAdjustmentFactor
  90. case "delay":
  91. return pickerSettingsProvider.settings.delay
  92. case "minuteInterval":
  93. return pickerSettingsProvider.settings.minuteInterval
  94. case "high":
  95. return pickerSettingsProvider.settings.high
  96. case "low":
  97. return pickerSettingsProvider.settings.low
  98. case "hours":
  99. return pickerSettingsProvider.settings.hours
  100. case "maxCarbs":
  101. return pickerSettingsProvider.settings.maxCarbs
  102. case "maxMealAbsorptionTime":
  103. return pickerSettingsProvider.settings.maxMealAbsorptionTime
  104. case "maxFat":
  105. return pickerSettingsProvider.settings.maxFat
  106. case "maxProtein":
  107. return pickerSettingsProvider.settings.maxProtein
  108. case "overrideFactor":
  109. return pickerSettingsProvider.settings.overrideFactor
  110. case "fattyMealFactor":
  111. return pickerSettingsProvider.settings.fattyMealFactor
  112. case "sweetMealFactor":
  113. return pickerSettingsProvider.settings.sweetMealFactor
  114. case "maxIOB":
  115. return pickerSettingsProvider.settings.maxIOB
  116. case "maxDailySafetyMultiplier":
  117. return pickerSettingsProvider.settings.maxDailySafetyMultiplier
  118. case "currentBasalSafetyMultiplier":
  119. return pickerSettingsProvider.settings.currentBasalSafetyMultiplier
  120. case "autosensMax":
  121. return pickerSettingsProvider.settings.autosensMax
  122. case "autosensMin":
  123. return pickerSettingsProvider.settings.autosensMin
  124. case "smbDeliveryRatio":
  125. return pickerSettingsProvider.settings.smbDeliveryRatio
  126. case "halfBasalExerciseTarget":
  127. return pickerSettingsProvider.settings.halfBasalExerciseTarget
  128. case "maxCOB":
  129. return pickerSettingsProvider.settings.maxCOB
  130. case "min5mCarbimpact":
  131. return pickerSettingsProvider.settings.min5mCarbimpact
  132. case "remainingCarbsFraction":
  133. return pickerSettingsProvider.settings.remainingCarbsFraction
  134. case "remainingCarbsCap":
  135. return pickerSettingsProvider.settings.remainingCarbsCap
  136. case "maxSMBBasalMinutes":
  137. return pickerSettingsProvider.settings.maxSMBBasalMinutes
  138. case "maxUAMSMBBasalMinutes":
  139. return pickerSettingsProvider.settings.maxUAMSMBBasalMinutes
  140. case "smbInterval":
  141. return pickerSettingsProvider.settings.smbInterval
  142. case "bolusIncrement":
  143. return pickerSettingsProvider.settings.bolusIncrement
  144. case "insulinPeakTime":
  145. return pickerSettingsProvider.settings.insulinPeakTime
  146. case "carbsReqThreshold":
  147. return pickerSettingsProvider.settings.carbsReqThreshold
  148. case "noisyCGMTargetMultiplier":
  149. return pickerSettingsProvider.settings.noisyCGMTargetMultiplier
  150. case "maxDeltaBGthreshold":
  151. return pickerSettingsProvider.settings.maxDeltaBGthreshold
  152. case "adjustmentFactor":
  153. return pickerSettingsProvider.settings.adjustmentFactor
  154. case "adjustmentFactorSigmoid":
  155. return pickerSettingsProvider.settings.adjustmentFactorSigmoid
  156. case "weightPercentage":
  157. return pickerSettingsProvider.settings.weightPercentage
  158. case "enableSMB_high_bg_target":
  159. return pickerSettingsProvider.settings.enableSMB_high_bg_target
  160. case "threshold_setting":
  161. return pickerSettingsProvider.settings.threshold_setting
  162. case "updateInterval":
  163. return pickerSettingsProvider.settings.updateInterval
  164. case "dia":
  165. return pickerSettingsProvider.settings.dia
  166. case "maxBolus":
  167. return pickerSettingsProvider.settings.maxBolus
  168. case "maxBasal":
  169. return pickerSettingsProvider.settings.maxBasal
  170. default:
  171. return nil
  172. }
  173. }
  174. private func pickerView(
  175. label: String,
  176. displayPicker: Binding<Bool>,
  177. setting: PickerSetting,
  178. decimalValue: Binding<Decimal>
  179. ) -> some View {
  180. VStack {
  181. HStack {
  182. Text(label)
  183. Spacer()
  184. displayText(for: setting, decimalValue: decimalValue.wrappedValue)
  185. .foregroundColor(!displayPicker.wrappedValue ? .primary : .accentColor)
  186. .onTapGesture {
  187. displayPicker.wrappedValue.toggle()
  188. }
  189. }.padding(.top)
  190. if displayPicker.wrappedValue {
  191. Picker(selection: decimalValue, label: Text(label)) {
  192. ForEach(pickerSettingsProvider.generatePickerValues(from: setting, units: self.units), id: \.self) { value in
  193. displayText(for: setting, decimalValue: value).tag(value)
  194. }
  195. }
  196. .pickerStyle(WheelPickerStyle())
  197. .frame(maxWidth: .infinity)
  198. }
  199. }
  200. }
  201. private func displayText(for setting: PickerSetting, decimalValue: Decimal) -> Text {
  202. switch setting.type {
  203. case .glucose:
  204. let displayValue = units == .mmolL ? decimalValue.asMmolL : decimalValue
  205. return Text("\(displayValue.description) \(units.rawValue)")
  206. case .factor:
  207. return Text("\(decimalValue * 100) \(String(localized: "%", comment: "Percentage symbol"))")
  208. case .insulinUnit:
  209. return Text("\(decimalValue) \(String(localized: "U", comment: "Insulin unit abbreviation"))")
  210. case .insulinUnitPerHour:
  211. return Text("\(decimalValue) \(String(localized: "U/hr", comment: "Insulin unit per hour abbreviation"))")
  212. case .gram:
  213. return Text("\(decimalValue) \(String(localized: "g", comment: "Gram abbreviation"))")
  214. case .minute:
  215. return Text("\(decimalValue) \(String(localized: "min", comment: "Minutes abbreviation"))")
  216. case .hour:
  217. return Text("\(decimalValue) \(String(localized: "hr", comment: "Hours abbreviation"))")
  218. }
  219. }
  220. private func toggleView(label: String, isOn: Binding<Bool>) -> some View {
  221. HStack {
  222. Toggle(isOn: isOn) {
  223. Text(label)
  224. }
  225. }.padding(.top)
  226. }
  227. private func hintSection(
  228. miniHint: String,
  229. shouldDisplayHint: Binding<Bool>,
  230. verboseHint: VerboseHint,
  231. miniHintColor: Color = .secondary
  232. ) -> some View {
  233. HStack(alignment: .center) {
  234. Text(miniHint)
  235. .font(.footnote)
  236. .foregroundColor(miniHintColor)
  237. .lineLimit(nil)
  238. Spacer()
  239. Button(action: {
  240. shouldDisplayHint.wrappedValue.toggle()
  241. selectedVerboseHint = shouldDisplayHint.wrappedValue ? verboseHint : nil
  242. }) {
  243. HStack {
  244. Image(systemName: "questionmark.circle")
  245. }
  246. }
  247. .buttonStyle(BorderlessButtonStyle())
  248. }.padding(.vertical)
  249. }
  250. }