EditOverrideForm.swift 30 KB

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