EditTempTargetForm.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import Foundation
  2. import SwiftUI
  3. struct EditTempTargetForm: View {
  4. @ObservedObject var tempTarget: TempTargetStored
  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 target: Decimal
  10. @State private var duration: Decimal
  11. @State private var date: Date
  12. @State private var halfBasalTarget: Decimal
  13. @State private var percentage: Decimal
  14. @State private var hasChanges = false
  15. @State private var showAlert = false
  16. @State private var isUsingSlider = false
  17. init(tempTargetToEdit: TempTargetStored, state: OverrideConfig.StateModel) {
  18. tempTarget = tempTargetToEdit
  19. _state = StateObject(wrappedValue: state)
  20. _name = State(initialValue: tempTargetToEdit.name ?? "")
  21. _target = State(initialValue: tempTargetToEdit.target?.decimalValue ?? 0)
  22. _duration = State(initialValue: tempTargetToEdit.duration?.decimalValue ?? 0)
  23. _date = State(initialValue: tempTargetToEdit.date ?? Date())
  24. _halfBasalTarget = State(initialValue: tempTargetToEdit.halfBasalTarget?.decimalValue ?? 160)
  25. let normalTarget: Decimal = 100
  26. if let hbt = tempTargetToEdit.halfBasalTarget?.decimalValue {
  27. let H = hbt
  28. let N: Decimal = normalTarget
  29. var T = tempTargetToEdit.target?.decimalValue ?? 0
  30. if state.units == .mmolL {
  31. T = T.asMgdL
  32. }
  33. let denominator = H - (2 * N) + T
  34. if denominator != 0 {
  35. let ratio = (H - N) / denominator
  36. _percentage = State(initialValue: ratio * 100)
  37. } else {
  38. _percentage = State(initialValue: 100)
  39. }
  40. } else {
  41. _percentage = State(initialValue: 100)
  42. }
  43. }
  44. var color: LinearGradient {
  45. colorScheme == .dark ? LinearGradient(
  46. gradient: Gradient(colors: [
  47. Color.bgDarkBlue,
  48. Color.bgDarkerDarkBlue
  49. ]),
  50. startPoint: .top,
  51. endPoint: .bottom
  52. ) :
  53. LinearGradient(
  54. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  55. startPoint: .top,
  56. endPoint: .bottom
  57. )
  58. }
  59. private var formatter: NumberFormatter {
  60. let formatter = NumberFormatter()
  61. formatter.numberStyle = .decimal
  62. formatter.maximumFractionDigits = 0
  63. return formatter
  64. }
  65. private var glucoseFormatter: NumberFormatter {
  66. let formatter = NumberFormatter()
  67. formatter.numberStyle = .decimal
  68. if state.units == .mmolL {
  69. formatter.maximumFractionDigits = 1
  70. } else {
  71. formatter.maximumFractionDigits = 0
  72. }
  73. formatter.roundingMode = .halfUp
  74. return formatter
  75. }
  76. var body: some View {
  77. NavigationView {
  78. Form {
  79. editTempTarget()
  80. saveButton
  81. }.scrollContentBackground(.hidden)
  82. .background(color)
  83. .navigationTitle("Edit Temp Target")
  84. .navigationBarTitleDisplayMode(.inline)
  85. .navigationBarItems(leading: Button("Close") {
  86. presentationMode.wrappedValue.dismiss()
  87. })
  88. .onDisappear {
  89. if !hasChanges {
  90. // Reset UI changes
  91. resetValues()
  92. }
  93. }
  94. .alert(isPresented: $state.showInvalidTargetAlert) {
  95. Alert(
  96. title: Text("Invalid Input"),
  97. message: Text("\(state.alertMessage)"),
  98. dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
  99. )
  100. }
  101. }
  102. }
  103. @ViewBuilder private func editTempTarget() -> some View {
  104. Section {
  105. VStack {
  106. TextField("Name", text: $name)
  107. .onChange(of: name) { _ in hasChanges = true }
  108. }
  109. } header: {
  110. Text("Name")
  111. }.listRowBackground(Color.chart)
  112. if state.computeSliderLow() != state.computeSliderHigh() {
  113. Section {
  114. VStack {
  115. VStack {
  116. Text("\(percentage.formatted(.number.precision(.fractionLength(0)))) % Insulin")
  117. .foregroundColor(isUsingSlider ? .orange : Color.tabBar)
  118. .font(.largeTitle)
  119. Slider(value: Binding(
  120. get: {
  121. Double(truncating: percentage as NSNumber)
  122. },
  123. set: { newValue in
  124. percentage = Decimal(newValue)
  125. hasChanges = true
  126. // Calculate the halfBasalTarget based on the new percentage value
  127. let ratio = Decimal(Int(percentage) / 100)
  128. let normalTarget: Decimal = 100
  129. var target: Decimal = target
  130. if state.units == .mmolL {
  131. target = target.asMgdL
  132. }
  133. if ratio != 1 {
  134. let hbtcalc = ((2 * ratio * normalTarget) - normalTarget - (ratio * target)) / (ratio - 1)
  135. halfBasalTarget = hbtcalc
  136. } else {
  137. halfBasalTarget = normalTarget
  138. }
  139. }
  140. ), in: Double(state.computeSliderLow()) ... Double(state.computeSliderHigh()), step: 5) {}
  141. minimumValueLabel: {
  142. Text("\(state.computeSliderLow(), specifier: "%.0f")%")
  143. }
  144. maximumValueLabel: {
  145. Text("\(state.computeSliderHigh(), specifier: "%.0f")%")
  146. }
  147. onEditingChanged: { editing in
  148. isUsingSlider = editing
  149. state.halfBasalTarget = Decimal(state.computeHalfBasalTarget())
  150. }
  151. Divider()
  152. Text(
  153. state
  154. .units == .mgdL ?
  155. "Half Basal Exercise Target at: \(halfBasalTarget.formatted(.number.precision(.fractionLength(0)))) mg/dl" :
  156. "Half Basal Exercise Target at: \(halfBasalTarget.asMmolL.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))) mmol/L"
  157. )
  158. .foregroundColor(.secondary)
  159. .font(.caption).italic()
  160. }
  161. }
  162. } header: {
  163. Text("% Insulin")
  164. } footer: {
  165. Text("The Slider values are limited to your Autosens Min and Max Settings!")
  166. }.listRowBackground(Color.chart)
  167. }
  168. Section {
  169. HStack {
  170. Text("Target")
  171. Spacer()
  172. TextFieldWithToolBar(
  173. text: Binding(
  174. get: { target },
  175. set: {
  176. target = $0
  177. hasChanges = true
  178. }
  179. ),
  180. placeholder: "0",
  181. numberFormatter: glucoseFormatter
  182. )
  183. Text(state.units.rawValue).foregroundColor(.secondary)
  184. }
  185. HStack {
  186. Text("Duration")
  187. Spacer()
  188. TextFieldWithToolBar(
  189. text: Binding(
  190. get: { duration },
  191. set: {
  192. duration = $0
  193. hasChanges = true
  194. }
  195. ),
  196. placeholder: "0",
  197. numberFormatter: formatter
  198. )
  199. Text("minutes").foregroundColor(.secondary)
  200. }
  201. DatePicker("Date", selection: $date)
  202. .onChange(of: date) { _ in hasChanges = true }
  203. }.listRowBackground(Color.chart)
  204. }
  205. private var saveButton: some View {
  206. HStack {
  207. Spacer()
  208. Button(action: {
  209. if !state.isInputInvalid(target: target) {
  210. saveChanges()
  211. do {
  212. guard let moc = tempTarget.managedObjectContext else { return }
  213. guard moc.hasChanges else { return }
  214. try moc.save()
  215. // Disable previous active Temp Target and update View
  216. if let currentActiveTempTarget = state.currentActiveTempTarget {
  217. Task {
  218. await state.disableAllActiveOverrides(
  219. except: currentActiveTempTarget.objectID,
  220. createOverrideRunEntry: false
  221. )
  222. state.updateLatestTempTargetConfiguration()
  223. }
  224. }
  225. hasChanges = false
  226. presentationMode.wrappedValue.dismiss()
  227. } catch {
  228. debugPrint("Failed to edit Temp Target")
  229. }
  230. }
  231. }, label: {
  232. Text("Save")
  233. })
  234. .disabled(!hasChanges)
  235. .frame(maxWidth: .infinity, alignment: .center)
  236. .tint(.white)
  237. Spacer()
  238. }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
  239. }
  240. private func saveChanges() {
  241. tempTarget.name = name
  242. tempTarget.target = NSDecimalNumber(decimal: target)
  243. tempTarget.duration = NSDecimalNumber(decimal: duration)
  244. tempTarget.date = date
  245. tempTarget.isUploadedToNS = false
  246. tempTarget.halfBasalTarget = NSDecimalNumber(decimal: halfBasalTarget)
  247. }
  248. private func resetValues() {
  249. name = tempTarget.name ?? ""
  250. target = tempTarget.target?.decimalValue ?? 0
  251. duration = tempTarget.duration?.decimalValue ?? 0
  252. date = tempTarget.date ?? Date()
  253. }
  254. }