EditOverrideForm.swift 28 KB

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