AlgorithmSettingsSubstepView.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. //
  2. // AlgorithmSettingsSubstepView.swift
  3. // Trio
  4. //
  5. // Created by Cengiz Deniz on 15.04.25.
  6. //
  7. import SwiftUI
  8. struct AlgorithmSettingsSubstepView<Substep: AlgorithmSubstepProtocol & RawRepresentable>: View where Substep.RawValue == Int {
  9. @Bindable var state: Onboarding.StateModel
  10. let substep: Substep
  11. @State private var shouldDisplayPicker: Bool = false
  12. @State private var decimalPlaceholder: Decimal = 0.0
  13. @State private var booleanPlaceholder: Bool = false
  14. private let settingsProvider = PickerSettingsProvider.shared
  15. private var shouldDisableRewindResetsAutosens: Bool {
  16. switch state.pumpOptionForOnboardingUnits {
  17. case .dana,
  18. .minimed:
  19. return false
  20. case .medtrum,
  21. .omnipodDash,
  22. .omnipodEros:
  23. return true
  24. }
  25. }
  26. var body: some View {
  27. VStack(alignment: .leading, spacing: 16) {
  28. Text(substep.title)
  29. .padding(.horizontal)
  30. .font(.title3)
  31. .bold()
  32. Text(substep.hint(units: state.units))
  33. .font(.subheadline)
  34. .multilineTextAlignment(.leading)
  35. .padding(.horizontal)
  36. .foregroundStyle(Color.secondary)
  37. if let step = substep.toAlgorithmSubstep() {
  38. switch step {
  39. case .autosensMin:
  40. algorithmSettingsInput(
  41. label: step.title,
  42. displayPicker: $shouldDisplayPicker,
  43. setting: settingsProvider.settings.autosensMin,
  44. decimalValue: $state.autosensMin,
  45. booleanValue: $booleanPlaceholder,
  46. type: OnboardingInputSectionType.decimal
  47. )
  48. case .autosensMax:
  49. algorithmSettingsInput(
  50. label: step.title,
  51. displayPicker: $shouldDisplayPicker,
  52. setting: settingsProvider.settings.autosensMax,
  53. decimalValue: $state.autosensMax,
  54. booleanValue: $booleanPlaceholder,
  55. type: OnboardingInputSectionType.decimal
  56. )
  57. case .rewindResetsAutosens:
  58. algorithmSettingsInput(
  59. label: step.title,
  60. displayPicker: $shouldDisplayPicker,
  61. setting: nil,
  62. decimalValue: $decimalPlaceholder,
  63. booleanValue: $state.rewindResetsAutosens,
  64. type: OnboardingInputSectionType.boolean,
  65. disabled: shouldDisableRewindResetsAutosens
  66. )
  67. case .enableSMBAlways:
  68. algorithmSettingsInput(
  69. label: step.title,
  70. displayPicker: $shouldDisplayPicker,
  71. setting: nil,
  72. decimalValue: $decimalPlaceholder,
  73. booleanValue: $state.enableSMBAlways,
  74. type: OnboardingInputSectionType.boolean
  75. )
  76. case .enableSMBWithCOB:
  77. algorithmSettingsInput(
  78. label: step.title,
  79. displayPicker: $shouldDisplayPicker,
  80. setting: nil,
  81. decimalValue: $decimalPlaceholder,
  82. booleanValue: $state.enableSMBWithCOB,
  83. type: OnboardingInputSectionType.boolean,
  84. disabled: state.enableSMBAlways
  85. )
  86. case .enableSMBWithTempTarget:
  87. algorithmSettingsInput(
  88. label: step.title,
  89. displayPicker: $shouldDisplayPicker,
  90. setting: nil,
  91. decimalValue: $decimalPlaceholder,
  92. booleanValue: $state.enableSMBWithTempTarget,
  93. type: OnboardingInputSectionType.boolean,
  94. disabled: state.enableSMBAlways
  95. )
  96. case .enableSMBAfterCarbs:
  97. algorithmSettingsInput(
  98. label: step.title,
  99. displayPicker: $shouldDisplayPicker,
  100. setting: nil,
  101. decimalValue: $decimalPlaceholder,
  102. booleanValue: $state.enableSMBAfterCarbs,
  103. type: OnboardingInputSectionType.boolean,
  104. disabled: state.enableSMBAlways
  105. )
  106. case .enableSMBWithHighGlucoseTarget:
  107. algorithmSettingsInput(
  108. label: step.title,
  109. displayPicker: $shouldDisplayPicker,
  110. setting: nil,
  111. decimalValue: $decimalPlaceholder,
  112. booleanValue: $state.enableSMBWithHighGlucoseTarget,
  113. type: OnboardingInputSectionType.boolean,
  114. disabled: state.enableSMBAlways
  115. )
  116. if state.enableSMBWithHighGlucoseTarget {
  117. algorithmSettingsInput(
  118. label: String(localized: "High Glucose Target"),
  119. displayPicker: $shouldDisplayPicker,
  120. setting: settingsProvider.settings.enableSMB_high_bg_target,
  121. decimalValue: $state.highGlucoseTarget,
  122. booleanValue: $booleanPlaceholder,
  123. type: OnboardingInputSectionType.decimal,
  124. disabled: state.enableSMBAlways
  125. )
  126. }
  127. case .allowSMBWithHighTempTarget:
  128. algorithmSettingsInput(
  129. label: step.title,
  130. displayPicker: $shouldDisplayPicker,
  131. setting: nil,
  132. decimalValue: $decimalPlaceholder,
  133. booleanValue: $state.allowSMBWithHighTempTarget,
  134. type: OnboardingInputSectionType.boolean
  135. )
  136. case .enableUAM:
  137. algorithmSettingsInput(
  138. label: step.title,
  139. displayPicker: $shouldDisplayPicker,
  140. setting: nil,
  141. decimalValue: $decimalPlaceholder,
  142. booleanValue: $state.enableUAM,
  143. type: OnboardingInputSectionType.boolean
  144. )
  145. case .maxSMBMinutes:
  146. algorithmSettingsInput(
  147. label: step.title,
  148. displayPicker: $shouldDisplayPicker,
  149. setting: settingsProvider.settings.maxSMBBasalMinutes,
  150. decimalValue: $state.maxSMBMinutes,
  151. booleanValue: $booleanPlaceholder,
  152. type: OnboardingInputSectionType.decimal
  153. )
  154. case .maxUAMMinutes:
  155. algorithmSettingsInput(
  156. label: step.title,
  157. displayPicker: $shouldDisplayPicker,
  158. setting: settingsProvider.settings.maxUAMSMBBasalMinutes,
  159. decimalValue: $state.maxUAMMinutes,
  160. booleanValue: $booleanPlaceholder,
  161. type: OnboardingInputSectionType.decimal
  162. )
  163. case .maxDeltaGlucoseThreshold:
  164. algorithmSettingsInput(
  165. label: step.title,
  166. displayPicker: $shouldDisplayPicker,
  167. setting: settingsProvider.settings.maxDeltaBGthreshold,
  168. decimalValue: $state.maxDeltaGlucoseThreshold,
  169. booleanValue: $booleanPlaceholder,
  170. type: OnboardingInputSectionType.decimal
  171. )
  172. case .highTempTargetRaisesSensitivity:
  173. algorithmSettingsInput(
  174. label: step.title,
  175. displayPicker: $shouldDisplayPicker,
  176. setting: nil,
  177. decimalValue: $decimalPlaceholder,
  178. booleanValue: $state.highTempTargetRaisesSensitivity,
  179. type: OnboardingInputSectionType.boolean
  180. )
  181. case .lowTempTargetLowersSensitivity:
  182. algorithmSettingsInput(
  183. label: step.title,
  184. displayPicker: $shouldDisplayPicker,
  185. setting: nil,
  186. decimalValue: $decimalPlaceholder,
  187. booleanValue: $state.lowTempTargetLowersSensitivity,
  188. type: OnboardingInputSectionType.boolean
  189. )
  190. case .sensitivityRaisesTarget:
  191. algorithmSettingsInput(
  192. label: step.title,
  193. displayPicker: $shouldDisplayPicker,
  194. setting: nil,
  195. decimalValue: $decimalPlaceholder,
  196. booleanValue: $state.sensitivityRaisesTarget,
  197. type: OnboardingInputSectionType.boolean
  198. )
  199. case .resistanceLowersTarget:
  200. algorithmSettingsInput(
  201. label: step.title,
  202. displayPicker: $shouldDisplayPicker,
  203. setting: nil,
  204. decimalValue: $decimalPlaceholder,
  205. booleanValue: $state.resistanceLowersTarget,
  206. type: OnboardingInputSectionType.boolean
  207. )
  208. case .halfBasalTarget:
  209. algorithmSettingsInput(
  210. label: step.title,
  211. displayPicker: $shouldDisplayPicker,
  212. setting: settingsProvider.settings.halfBasalExerciseTarget,
  213. decimalValue: $state.halfBasalTarget,
  214. booleanValue: $booleanPlaceholder,
  215. type: OnboardingInputSectionType.decimal
  216. )
  217. }
  218. }
  219. substep.description(units: state.units).eraseToAnyView()
  220. .font(.footnote)
  221. .foregroundStyle(.secondary)
  222. .padding(.horizontal)
  223. .multilineTextAlignment(.leading)
  224. }
  225. .onAppear {
  226. // Ensure picker view is closed, when switching between setting steps
  227. shouldDisplayPicker = false
  228. }
  229. }
  230. @ViewBuilder private func algorithmSettingsInput(
  231. label: String,
  232. displayPicker: Binding<Bool>,
  233. setting: PickerSetting?,
  234. decimalValue: Binding<Decimal>,
  235. booleanValue: Binding<Bool>,
  236. type: OnboardingInputSectionType,
  237. disabled: Bool = false /// parameter only relevant for `Enable SMB Always` dependent settings
  238. ) -> some View {
  239. VStack {
  240. VStack {
  241. switch type {
  242. case .boolean:
  243. Toggle(isOn: booleanValue) {
  244. Text(label)
  245. }.tint(Color.accentColor)
  246. .disabled(disabled)
  247. case .decimal:
  248. Group {
  249. HStack {
  250. Text(label)
  251. Spacer()
  252. displayText(for: substep, decimalValue: decimalValue.wrappedValue, units: state.units)
  253. .foregroundColor(!displayPicker.wrappedValue ? .primary : .accentColor)
  254. .onTapGesture {
  255. displayPicker.wrappedValue.toggle()
  256. }
  257. }.disabled(disabled)
  258. if displayPicker.wrappedValue {
  259. Picker(selection: decimalValue, label: Text(label)) {
  260. if let setting = setting {
  261. ForEach(
  262. settingsProvider.generatePickerValues(from: setting, units: state.units),
  263. id: \.self
  264. ) { value in
  265. displayText(for: substep, decimalValue: value, units: state.units).tag(value)
  266. }
  267. }
  268. }
  269. .disabled(disabled)
  270. .pickerStyle(WheelPickerStyle())
  271. .frame(maxWidth: .infinity)
  272. }
  273. }
  274. }
  275. }
  276. .padding()
  277. .background(Color.chart.opacity(0.65))
  278. .cornerRadius(10)
  279. }
  280. }
  281. private func displayText(for substep: Substep, decimalValue: Decimal, units: GlucoseUnits) -> Text {
  282. guard let step = substep.toAlgorithmSubstep() else {
  283. return Text(decimalValue.description)
  284. }
  285. switch step {
  286. case .autosensMax,
  287. .autosensMin,
  288. .maxDeltaGlucoseThreshold:
  289. return Text("\(decimalValue * 100) \(String(localized: "%"))")
  290. case .enableSMBWithHighGlucoseTarget,
  291. .halfBasalTarget:
  292. let displayValue = units == .mmolL ? decimalValue.asMmolL : decimalValue
  293. return Text("\(displayValue.description) \(units.rawValue)")
  294. case .maxSMBMinutes,
  295. .maxUAMMinutes:
  296. return Text("\(decimalValue) \(String(localized: "min"))")
  297. default:
  298. return Text("") // not needed, because input type is boolean
  299. }
  300. }
  301. }