EditOverrideForm.swift 30 KB

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