EditOverrideForm.swift 30 KB

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