EditOverrideForm.swift 30 KB

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