EditOverrideForm.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. import Foundation
  2. import SwiftUI
  3. struct EditOverrideForm: View {
  4. @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
  5. @ObservedObject var override: OverrideStored
  6. @Environment(\.presentationMode) var presentationMode
  7. @Environment(\.colorScheme) var colorScheme
  8. @Bindable var state: OverrideConfig.StateModel
  9. @State private var name: String
  10. @State private var percentage: Double
  11. @State private var indefinite: Bool
  12. @State private var duration: Decimal
  13. @State private var target: Decimal?
  14. @State private var advancedSettings: Bool
  15. @State private var smbIsOff: Bool
  16. @State private var smbIsScheduledOff: Bool
  17. @State private var start: Decimal?
  18. @State private var end: Decimal?
  19. @State private var isfAndCr: Bool
  20. @State private var isf: Bool
  21. @State private var cr: Bool
  22. @State private var smbMinutes: Decimal?
  23. @State private var uamMinutes: Decimal?
  24. @State private var selectedIsfCrOption: IsfAndOrCrOptions
  25. @State private var selectedDisableSmbOption: DisableSmbOptions
  26. @State private var hasChanges = false
  27. @State private var isEditing = false
  28. @State private var target_override = false
  29. @State private var percentageStep: Int = 5
  30. @State private var displayPickerPercentage: Bool = false
  31. @State private var displayPickerDuration: Bool = false
  32. @State private var targetStep: Decimal = 5
  33. @State private var displayPickerTarget: Bool = false
  34. @State private var displayPickerDisableSmbSchedule: Bool = false
  35. @State private var displayPickerSmbMinutes: Bool = false
  36. init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) {
  37. override = overrideToEdit
  38. _state = Bindable(wrappedValue: state)
  39. _name = State(initialValue: overrideToEdit.name ?? "")
  40. _percentage = State(initialValue: overrideToEdit.percentage)
  41. _indefinite = State(initialValue: overrideToEdit.indefinite)
  42. _duration = State(initialValue: overrideToEdit.duration?.decimalValue ?? 0)
  43. _target = State(initialValue: overrideToEdit.target?.decimalValue)
  44. _target_override = State(initialValue: overrideToEdit.target != nil && overrideToEdit.target?.decimalValue != 0)
  45. _advancedSettings = State(initialValue: overrideToEdit.advancedSettings)
  46. _smbIsOff = State(initialValue: overrideToEdit.smbIsOff)
  47. _smbIsScheduledOff = State(initialValue: overrideToEdit.smbIsScheduledOff)
  48. _start = State(initialValue: overrideToEdit.start?.decimalValue)
  49. _end = State(initialValue: overrideToEdit.end?.decimalValue)
  50. _isfAndCr = State(initialValue: overrideToEdit.isfAndCr)
  51. _isf = State(initialValue: overrideToEdit.isf)
  52. _cr = State(initialValue: overrideToEdit.cr)
  53. _selectedIsfCrOption = State(
  54. initialValue: overrideToEdit.isfAndCr ? .isfAndCr
  55. : (overrideToEdit.isf ? .isf : (overrideToEdit.cr ? .cr : .nothing))
  56. )
  57. _selectedDisableSmbOption = State(
  58. initialValue: overrideToEdit.smbIsScheduledOff ? .disableOnSchedule
  59. : (overrideToEdit.smbIsOff ? .disable : .dontDisable)
  60. )
  61. _smbMinutes = State(initialValue: overrideToEdit.smbMinutes?.decimalValue)
  62. _uamMinutes = State(initialValue: overrideToEdit.uamMinutes?.decimalValue)
  63. }
  64. var color: LinearGradient {
  65. colorScheme == .dark ? LinearGradient(
  66. gradient: Gradient(colors: [
  67. Color.bgDarkBlue,
  68. Color.bgDarkerDarkBlue
  69. ]),
  70. startPoint: .top,
  71. endPoint: .bottom
  72. ) :
  73. LinearGradient(
  74. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  75. startPoint: .top,
  76. endPoint: .bottom
  77. )
  78. }
  79. private var percentageSelection: Binding<Double> {
  80. Binding<Double>(
  81. get: {
  82. let value = floor(percentage / Double(percentageStep)) * Double(percentageStep)
  83. return max(10, min(value, 200))
  84. },
  85. set: {
  86. percentage = $0
  87. hasChanges = true
  88. }
  89. )
  90. }
  91. var body: some View {
  92. NavigationView {
  93. List {
  94. editOverride()
  95. saveButton
  96. }
  97. .listSectionSpacing(10)
  98. .padding(.top, 30)
  99. .ignoresSafeArea(edges: .top)
  100. .scrollContentBackground(.hidden).background(color)
  101. .navigationTitle("Edit Override")
  102. .navigationBarTitleDisplayMode(.inline)
  103. .toolbar {
  104. ToolbarItem(placement: .topBarLeading) {
  105. Button(action: {
  106. presentationMode.wrappedValue.dismiss()
  107. }, label: {
  108. Text("Cancel")
  109. })
  110. }
  111. ToolbarItem(placement: .topBarTrailing) {
  112. Button(
  113. action: {
  114. state.isHelpSheetPresented.toggle()
  115. },
  116. label: {
  117. Image(systemName: "questionmark.circle")
  118. }
  119. )
  120. }
  121. }
  122. .onAppear { targetStep = state.units == .mgdL ? 5 : 9 }
  123. .onDisappear {
  124. if !hasChanges {
  125. // Reset UI changes
  126. resetValues()
  127. }
  128. }
  129. .sheet(isPresented: $state.isHelpSheetPresented) {
  130. NavigationStack {
  131. List {
  132. Text("Lorem Ipsum Dolor Sit Amet")
  133. }
  134. .padding(.trailing, 10)
  135. .navigationBarTitle("Help", displayMode: .inline)
  136. Button { state.isHelpSheetPresented.toggle() }
  137. label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
  138. .buttonStyle(.bordered)
  139. .padding(.top)
  140. }
  141. .padding()
  142. .presentationDetents(
  143. [.fraction(0.9), .large],
  144. selection: $state.helpSheetDetent
  145. )
  146. }
  147. }
  148. }
  149. @ViewBuilder private func editOverride() -> some View {
  150. Group {
  151. if override.name != nil {
  152. Section {
  153. HStack {
  154. Text("Name")
  155. Spacer()
  156. TextField("Name", text: $name)
  157. .onChange(of: name) { hasChanges = true }
  158. .multilineTextAlignment(.trailing)
  159. }
  160. }
  161. .listRowBackground(Color.chart)
  162. }
  163. Section {
  164. Toggle(isOn: $indefinite) { Text("Enable Indefinitely") }
  165. .onChange(of: indefinite) { hasChanges = true }
  166. if !indefinite {
  167. HStack {
  168. Text("Duration")
  169. Spacer()
  170. Text(formatHrMin(Int(truncating: duration as NSNumber)))
  171. .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
  172. }
  173. .onTapGesture {
  174. displayPickerDuration = toggleScrollWheel(displayPickerDuration)
  175. }
  176. if displayPickerDuration {
  177. HStack {
  178. Picker(
  179. selection: Binding(
  180. get: {
  181. Int(truncating: duration as NSNumber) / 60
  182. },
  183. set: {
  184. let minutes = Int(truncating: duration as NSNumber) % 60
  185. let totalMinutes = $0 * 60 + minutes
  186. duration = Decimal(totalMinutes)
  187. hasChanges = true
  188. }
  189. ),
  190. label: Text("")
  191. ) {
  192. ForEach(0 ..< 24) { hour in
  193. Text("\(hour) hr").tag(hour)
  194. }
  195. }
  196. .pickerStyle(WheelPickerStyle())
  197. .frame(maxWidth: .infinity)
  198. Picker(
  199. selection: Binding(
  200. get: {
  201. Int(truncating: duration as NSNumber) %
  202. 60 // Convert Decimal to Int for modulus operation
  203. },
  204. set: {
  205. duration = Decimal((Int(truncating: duration as NSNumber) / 60) * 60 + $0)
  206. hasChanges = true
  207. }
  208. ),
  209. label: Text("")
  210. ) {
  211. ForEach(Array(stride(from: 0, through: 55, by: 5)), id: \.self) { minute in
  212. Text("\(minute) min").tag(minute)
  213. }
  214. }
  215. .pickerStyle(WheelPickerStyle())
  216. .frame(maxWidth: .infinity)
  217. }
  218. .listRowSeparator(.hidden, edges: .top)
  219. }
  220. }
  221. }
  222. .listRowBackground(Color.chart)
  223. // Percentage Picker
  224. Section(footer: percentageDescription(percentage)) {
  225. HStack {
  226. Text("Change Basal Rate by")
  227. Spacer()
  228. Text("\(percentage.formatted(.number)) %")
  229. .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)
  230. }
  231. .onTapGesture {
  232. displayPickerPercentage = toggleScrollWheel(displayPickerPercentage)
  233. }
  234. if displayPickerPercentage {
  235. HStack {
  236. // Radio buttons and text on the left side
  237. VStack(alignment: .leading) {
  238. // Radio buttons for step iteration
  239. ForEach([1, 5], id: \.self) { step in
  240. RadioButton(isSelected: percentageStep == step, label: "\(step) %") {
  241. percentageStep = step
  242. percentage = OverrideConfig.StateModel.roundOverridePercentageToStep(percentage, step)
  243. }
  244. .padding(.top, 10)
  245. }
  246. }
  247. .frame(maxWidth: .infinity)
  248. Spacer()
  249. // Picker on the right side
  250. Picker(
  251. selection: percentageSelection,
  252. label: Text("")
  253. ) {
  254. ForEach(
  255. Array(stride(from: 40.0, through: 150.0, by: Double(percentageStep))),
  256. id: \.self
  257. ) { percent in
  258. Text("\(Int(percent)) %").tag(percent)
  259. }
  260. }
  261. .pickerStyle(WheelPickerStyle())
  262. .frame(maxWidth: .infinity)
  263. }
  264. .listRowSeparator(.hidden, edges: .top)
  265. }
  266. // Picker for ISF/CR settings
  267. Picker("Also Change", selection: $selectedIsfCrOption) {
  268. ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
  269. Text(option.rawValue).tag(option)
  270. }
  271. }
  272. .pickerStyle(MenuPickerStyle())
  273. .onChange(of: selectedIsfCrOption) { _, newValue in
  274. switch newValue {
  275. case .isfAndCr:
  276. isfAndCr = true
  277. isf = false
  278. cr = false
  279. case .isf:
  280. isfAndCr = false
  281. isf = true
  282. cr = false
  283. case .cr:
  284. isfAndCr = false
  285. isf = false
  286. cr = true
  287. case .nothing:
  288. isfAndCr = false
  289. isf = false
  290. cr = false
  291. }
  292. hasChanges = true
  293. }
  294. }
  295. .listRowBackground(Color.chart)
  296. Section {
  297. Toggle(isOn: $target_override) {
  298. Text("Override Target")
  299. }
  300. .onChange(of: target_override) {
  301. hasChanges = true
  302. }
  303. // Target Glucose Picker
  304. if target_override {
  305. let settingsProvider = PickerSettingsProvider.shared
  306. let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 72, max: 270, type: .glucose)
  307. TargetPicker(
  308. label: "Target Glucose",
  309. selection: Binding(
  310. get: { target ?? 100 },
  311. set: { target = $0 }
  312. ),
  313. options: settingsProvider.generatePickerValues(
  314. from: glucoseSetting,
  315. units: state.units,
  316. roundMinToStep: true
  317. ),
  318. units: state.units,
  319. hasChanges: $hasChanges,
  320. targetStep: $targetStep,
  321. displayPickerTarget: $displayPickerTarget,
  322. toggleScrollWheel: toggleScrollWheel
  323. )
  324. .onAppear {
  325. if target == 0 || target == nil {
  326. target = 100
  327. }
  328. }
  329. }
  330. }
  331. .listRowBackground(Color.chart)
  332. Section {
  333. // Picker for Disable SMB settings
  334. Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
  335. ForEach(DisableSmbOptions.allCases, id: \.self) { option in
  336. Text(option.rawValue).tag(option)
  337. }
  338. }
  339. .pickerStyle(MenuPickerStyle())
  340. .onChange(of: selectedDisableSmbOption) { _, newValue in
  341. switch newValue {
  342. case .dontDisable:
  343. smbIsOff = false
  344. smbIsScheduledOff = false
  345. case .disable:
  346. smbIsOff = true
  347. smbIsScheduledOff = false
  348. case .disableOnSchedule:
  349. smbIsOff = false
  350. smbIsScheduledOff = true
  351. }
  352. hasChanges = true
  353. }
  354. if smbIsScheduledOff {
  355. // First Hour SMBs Are Disabled
  356. HStack {
  357. Text("From")
  358. Spacer()
  359. Text(
  360. is24HourFormat() ? format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
  361. convertTo12HourFormat(Int(truncating: start! as NSNumber))
  362. )
  363. .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
  364. Spacer()
  365. Divider().frame(width: 1, height: 20)
  366. Spacer()
  367. Text("To")
  368. Spacer()
  369. Text(
  370. is24HourFormat() ? format24Hour(Int(truncating: end! as NSNumber)) + ":00" :
  371. convertTo12HourFormat(Int(truncating: end! as NSNumber))
  372. )
  373. .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
  374. }
  375. .onTapGesture {
  376. displayPickerDisableSmbSchedule = toggleScrollWheel(displayPickerDisableSmbSchedule)
  377. }
  378. if displayPickerDisableSmbSchedule {
  379. HStack {
  380. Picker(selection: Binding(
  381. get: { Int(truncating: start! as NSNumber) },
  382. set: {
  383. start = Decimal($0)
  384. hasChanges = true
  385. }
  386. ), label: Text("")) {
  387. if is24HourFormat() {
  388. ForEach(0 ..< 24, id: \.self) { hour in
  389. Text(format24Hour(hour) + ":00").tag(hour)
  390. }
  391. } else {
  392. ForEach(0 ..< 24, id: \.self) { hour in
  393. Text(convertTo12HourFormat(hour)).tag(hour)
  394. }
  395. }
  396. }
  397. .pickerStyle(WheelPickerStyle())
  398. .frame(maxWidth: .infinity)
  399. Picker(selection: Binding(
  400. get: { Int(truncating: end! as NSNumber) },
  401. set: {
  402. end = Decimal($0)
  403. hasChanges = true
  404. }
  405. ), label: Text("")) {
  406. if is24HourFormat() {
  407. ForEach(0 ..< 24, id: \.self) { hour in
  408. Text(format24Hour(hour) + ":00").tag(hour)
  409. }
  410. } else {
  411. ForEach(0 ..< 24, id: \.self) { hour in
  412. Text(convertTo12HourFormat(hour)).tag(hour)
  413. }
  414. }
  415. }
  416. .pickerStyle(WheelPickerStyle())
  417. .frame(maxWidth: .infinity)
  418. }
  419. .listRowSeparator(.hidden, edges: .top)
  420. }
  421. }
  422. }
  423. .listRowBackground(Color.chart)
  424. if !smbIsOff {
  425. Section {
  426. Toggle(isOn: $advancedSettings) {
  427. Text("Change Max SMB Minutes")
  428. }
  429. .onChange(of: advancedSettings) { hasChanges = true }
  430. if advancedSettings {
  431. // SMB Minutes Picker
  432. HStack {
  433. Text("SMB")
  434. Spacer()
  435. Text("\(smbMinutes?.formatted(.number) ?? "\(state.defaultSmbMinutes)") min")
  436. .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
  437. Spacer()
  438. Divider().frame(width: 1, height: 20)
  439. Spacer()
  440. Text("UAM")
  441. Spacer()
  442. Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min")
  443. .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
  444. }
  445. .onTapGesture {
  446. displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes)
  447. }
  448. if displayPickerSmbMinutes {
  449. HStack {
  450. Picker(
  451. selection: Binding(
  452. get: { smbMinutes ?? state.defaultSmbMinutes },
  453. set: {
  454. smbMinutes = $0
  455. hasChanges = true
  456. }
  457. ),
  458. label: Text("")
  459. ) {
  460. ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
  461. Text("\(minute) min").tag(Decimal(minute))
  462. }
  463. }
  464. .pickerStyle(WheelPickerStyle())
  465. .frame(maxWidth: .infinity)
  466. Picker(
  467. selection: Binding(
  468. get: { uamMinutes ?? state.defaultUamMinutes },
  469. set: {
  470. uamMinutes = $0
  471. hasChanges = true
  472. }
  473. ),
  474. label: Text("")
  475. ) {
  476. ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
  477. Text("\(minute) min").tag(Decimal(minute))
  478. }
  479. }
  480. .pickerStyle(WheelPickerStyle())
  481. .frame(maxWidth: .infinity)
  482. }
  483. .listRowSeparator(.hidden, edges: .top)
  484. }
  485. }
  486. }
  487. .listRowBackground(Color.chart)
  488. }
  489. }
  490. }
  491. private var saveButton: some View {
  492. let (isInvalid, errorMessage) = isOverrideInvalid()
  493. return Section(
  494. header:
  495. HStack {
  496. Spacer()
  497. Text(errorMessage ?? "").textCase(nil)
  498. .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
  499. Spacer()
  500. },
  501. content: {
  502. Button(action: {
  503. saveChanges()
  504. do {
  505. guard let moc = override.managedObjectContext else { return }
  506. guard moc.hasChanges else { return }
  507. try moc.save()
  508. Task {
  509. await nightscoutManager.uploadProfiles()
  510. }
  511. if let currentActiveOverride = state.currentActiveOverride {
  512. Task {
  513. await state.disableAllActiveOverrides(
  514. except: currentActiveOverride.objectID,
  515. createOverrideRunEntry: false
  516. )
  517. // Update View
  518. state.updateLatestOverrideConfiguration()
  519. }
  520. }
  521. hasChanges = false
  522. presentationMode.wrappedValue.dismiss()
  523. } catch {
  524. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
  525. }
  526. }, label: {
  527. Text("Save Override")
  528. })
  529. .disabled(isInvalid) // Disable button if changes are invalid
  530. .frame(maxWidth: .infinity, alignment: .center)
  531. .tint(.white)
  532. }
  533. )
  534. .listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
  535. }
  536. private func isOverrideInvalid() -> (Bool, String?) {
  537. let noDurationSpecified = !indefinite && duration == 0
  538. let targetZeroWithOverride = target_override && (target ?? 0 < 72 || target ?? 0 > 270)
  539. let allSettingsDefault = percentage == 100 && !target_override && !advancedSettings &&
  540. !smbIsOff && !smbIsScheduledOff
  541. if noDurationSpecified {
  542. return (true, "Enable indefinitely or set a duration.")
  543. }
  544. if targetZeroWithOverride {
  545. return (true, "Target glucose is out of range (\(state.units == .mgdL ? "72-270" : "4-14")).")
  546. }
  547. if allSettingsDefault {
  548. return (true, "All settings are at default values.")
  549. }
  550. if !hasChanges {
  551. return (true, nil)
  552. }
  553. return (false, nil)
  554. }
  555. private func saveChanges() {
  556. if !override.isPreset, hasChanges, name == (override.name ?? "") {
  557. override.name = "Custom Override"
  558. } else {
  559. override.name = name
  560. }
  561. override.percentage = percentage
  562. override.indefinite = indefinite
  563. override.duration = NSDecimalNumber(decimal: duration)
  564. override.target = target_override ? NSDecimalNumber(decimal: target ?? 100) : nil
  565. override.advancedSettings = advancedSettings
  566. override.smbIsOff = smbIsOff
  567. override.smbIsScheduledOff = smbIsScheduledOff
  568. override.start = start.map { NSDecimalNumber(decimal: $0) }
  569. override.end = end.map { NSDecimalNumber(decimal: $0) }
  570. override.isfAndCr = isfAndCr
  571. override.isf = isf
  572. override.cr = cr
  573. override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
  574. override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
  575. override.isUploadedToNS = false
  576. }
  577. private func resetValues() {
  578. name = override.name ?? ""
  579. percentage = override.percentage
  580. indefinite = override.indefinite
  581. duration = override.duration?.decimalValue ?? 0
  582. target = override.target?.decimalValue
  583. advancedSettings = override.advancedSettings
  584. smbIsOff = override.smbIsOff
  585. smbIsScheduledOff = override.smbIsScheduledOff
  586. start = override.start?.decimalValue
  587. end = override.end?.decimalValue
  588. isfAndCr = override.isfAndCr
  589. isf = override.isf
  590. cr = override.cr
  591. smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
  592. uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
  593. }
  594. private func toggleScrollWheel(_ toggle: Bool) -> Bool {
  595. displayPickerDuration = false
  596. displayPickerPercentage = false
  597. displayPickerTarget = false
  598. displayPickerDisableSmbSchedule = false
  599. displayPickerSmbMinutes = false
  600. return !toggle
  601. }
  602. }
  603. struct TargetPicker: View {
  604. let label: String
  605. @Binding var selection: Decimal
  606. let options: [Decimal]
  607. let units: GlucoseUnits
  608. @Binding var hasChanges: Bool
  609. @Binding var targetStep: Decimal
  610. @Binding var displayPickerTarget: Bool
  611. var toggleScrollWheel: (_ picker: Bool) -> Bool
  612. var body: some View {
  613. HStack {
  614. Text(label)
  615. Spacer()
  616. Text(
  617. (units == .mgdL ? selection.description : selection.formattedAsMmolL) + " " + units.rawValue
  618. )
  619. .foregroundColor(!displayPickerTarget ? .primary : .accentColor)
  620. }
  621. .onTapGesture {
  622. displayPickerTarget = toggleScrollWheel(displayPickerTarget)
  623. }
  624. if displayPickerTarget {
  625. HStack {
  626. // Radio buttons and text on the left side
  627. VStack(alignment: .leading) {
  628. // Radio buttons for step iteration
  629. let stepChoices: [Decimal] = units == .mgdL ? [1, 5] : [1, 9]
  630. ForEach(stepChoices, id: \.self) { step in
  631. let label = (units == .mgdL ? step.description : step.formattedAsMmolL) + " " +
  632. units.rawValue
  633. RadioButton(
  634. isSelected: targetStep == step,
  635. label: label
  636. ) {
  637. targetStep = step
  638. selection = OverrideConfig.StateModel.roundTargetToStep(selection, step)
  639. }
  640. .padding(.top, 10)
  641. }
  642. }
  643. .frame(maxWidth: .infinity)
  644. Spacer()
  645. // Picker on the right side
  646. Picker(selection: Binding(
  647. get: { OverrideConfig.StateModel.roundTargetToStep(selection, targetStep) },
  648. set: {
  649. selection = $0
  650. hasChanges = true
  651. }
  652. ), label: Text("")) {
  653. ForEach(options, id: \.self) { option in
  654. Text((units == .mgdL ? option.description : option.formattedAsMmolL) + " " + units.rawValue)
  655. .tag(option)
  656. }
  657. }
  658. .pickerStyle(WheelPickerStyle())
  659. .frame(maxWidth: .infinity)
  660. }
  661. .listRowSeparator(.hidden, edges: .top)
  662. }
  663. }
  664. }