EditOverrideForm.swift 31 KB

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