EditOverrideForm.swift 30 KB

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