AddTempTargetForm.swift 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import Foundation
  2. import SwiftUI
  3. struct AddTempTargetForm: View {
  4. @StateObject var state: OverrideConfig.StateModel
  5. @Environment(\.presentationMode) var presentationMode
  6. @Environment(\.colorScheme) var colorScheme
  7. @Environment(\.dismiss) var dismiss
  8. @State private var showAlert = false
  9. @State private var showPresetAlert = false
  10. @State private var alertString = ""
  11. @State private var isUsingSlider = false
  12. var color: LinearGradient {
  13. colorScheme == .dark ? LinearGradient(
  14. gradient: Gradient(colors: [
  15. Color.bgDarkBlue,
  16. Color.bgDarkerDarkBlue
  17. ]),
  18. startPoint: .top,
  19. endPoint: .bottom
  20. )
  21. :
  22. LinearGradient(
  23. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  24. startPoint: .top,
  25. endPoint: .bottom
  26. )
  27. }
  28. private var formatter: NumberFormatter {
  29. let formatter = NumberFormatter()
  30. formatter.numberStyle = .decimal
  31. formatter.maximumFractionDigits = 0
  32. return formatter
  33. }
  34. private var glucoseFormatter: NumberFormatter {
  35. let formatter = NumberFormatter()
  36. formatter.numberStyle = .decimal
  37. formatter.maximumFractionDigits = 0
  38. if state.units == .mmolL {
  39. formatter.maximumFractionDigits = 1
  40. }
  41. formatter.roundingMode = .halfUp
  42. return formatter
  43. }
  44. var body: some View {
  45. NavigationView {
  46. Form {
  47. addTempTarget()
  48. }.scrollContentBackground(.hidden).background(color)
  49. .navigationTitle("Add Temp Target")
  50. .navigationBarItems(trailing: Button("Cancel") {
  51. presentationMode.wrappedValue.dismiss()
  52. })
  53. .alert(
  54. "Start Temp Target",
  55. isPresented: $showAlert,
  56. actions: {
  57. Button("Cancel", role: .cancel) { state.isTempTargetEnabled = false }
  58. Button("Start Temp Target", role: .destructive) {
  59. Task {
  60. await setupAlertString()
  61. state.isTempTargetEnabled.toggle()
  62. await state.saveCustomTempTarget()
  63. await state.resetTempTargetState()
  64. dismiss()
  65. }
  66. }
  67. },
  68. message: {
  69. Text(alertString)
  70. }
  71. )
  72. }
  73. }
  74. @ViewBuilder private func addTempTarget() -> some View {
  75. Section {
  76. VStack {
  77. TextField("Name", text: $state.tempTargetName)
  78. }
  79. } header: {
  80. Text("Name")
  81. }.listRowBackground(Color.chart)
  82. Section {
  83. VStack {
  84. Text("\(state.percentage.formatted(.number)) % Insulin")
  85. .foregroundColor(isUsingSlider ? .orange : Color.tabBar)
  86. .font(.largeTitle)
  87. Slider(value: $state.percentage, in: computeSliderLow() ... computeSliderHigh(), step: 5) {}
  88. minimumValueLabel: {
  89. Text("\(computeSliderLow(), specifier: "%.0f")%")
  90. }
  91. maximumValueLabel: {
  92. Text("\(computeSliderHigh(), specifier: "%.0f")%")
  93. }
  94. onEditingChanged: { editing in
  95. isUsingSlider = editing
  96. state.halfBasalTarget = Decimal(state.computeHalfBasalTarget())
  97. }
  98. Divider()
  99. Text(
  100. state
  101. .units == .mgdL ?
  102. "Half Basal Exercise Target at: \(state.computeHalfBasalTarget().formatted(.number.precision(.fractionLength(0)))) mg/dl" :
  103. "Half Basal Exercise Target at: \(state.computeHalfBasalTarget().asMmolL.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))) mmol/L"
  104. )
  105. .foregroundColor(.secondary)
  106. .font(.caption).italic()
  107. }
  108. } header: {
  109. Text("% Insulin")
  110. }.listRowBackground(Color.chart)
  111. Section {
  112. HStack {
  113. Text("Target")
  114. Spacer()
  115. TextFieldWithToolBar(text: $state.tempTargetTarget, placeholder: "0", numberFormatter: glucoseFormatter)
  116. Text(state.units.rawValue).foregroundColor(.secondary)
  117. }
  118. HStack {
  119. Text("Duration")
  120. Spacer()
  121. TextFieldWithToolBar(text: $state.tempTargetDuration, placeholder: "0", numberFormatter: formatter)
  122. Text("minutes").foregroundColor(.secondary)
  123. }
  124. DatePicker("Date", selection: $state.date)
  125. HStack {
  126. Button {
  127. showAlert.toggle()
  128. }
  129. label: { Text("Enact") }
  130. .disabled(state.tempTargetDuration == 0)
  131. .buttonStyle(BorderlessButtonStyle())
  132. .font(.callout)
  133. .controlSize(.mini)
  134. Button {
  135. Task {
  136. await state.saveTempTargetPreset()
  137. dismiss()
  138. }
  139. }
  140. label: { Text("Save as preset") }
  141. .disabled(state.tempTargetDuration == 0)
  142. .tint(.orange)
  143. .frame(maxWidth: .infinity, alignment: .trailing)
  144. .buttonStyle(BorderlessButtonStyle())
  145. .controlSize(.mini)
  146. }
  147. } header: {
  148. Text("Add Custom Temp Target")
  149. }.listRowBackground(Color.chart)
  150. }
  151. private func setupAlertString() async {
  152. alertString =
  153. (
  154. state.tempTargetDuration > 0 ?
  155. (
  156. state
  157. .tempTargetDuration
  158. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
  159. " min."
  160. ) :
  161. NSLocalizedString(" infinite duration.", comment: "")
  162. ) +
  163. (
  164. state.tempTargetTarget == 0 ? "" :
  165. (" Target: " + state.tempTargetTarget.formatted() + " " + state.units.rawValue + ".")
  166. )
  167. +
  168. "\n\n"
  169. +
  170. NSLocalizedString(
  171. "Starting this Temp Target will change your profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Temp Target” will start your new Temp Target or edit your current active Temp Target.",
  172. comment: ""
  173. )
  174. }
  175. func computeSliderLow() -> Double {
  176. var minSens: Double = 15
  177. var target = state.low
  178. if state.units == .mmolL {
  179. target = Decimal(round(Double(state.low.asMgdL))) }
  180. if target == 0 { return minSens }
  181. if target < 100 ||
  182. (
  183. !state.settingsManager.preferences.highTemptargetRaisesSensitivity && !state.settingsManager.preferences
  184. .exerciseMode
  185. ) { minSens = 100 }
  186. return minSens
  187. }
  188. func computeSliderHigh() -> Double {
  189. var maxSens = Double(state.maxValue * 100)
  190. var target = state.low
  191. if target == 0 { return maxSens }
  192. if state.units == .mmolL {
  193. target = Decimal(round(Double(state.low.asMgdL))) }
  194. if target > 100 || !state.settingsManager.preferences.lowTemptargetLowersSensitivity { maxSens = 100 }
  195. return maxSens
  196. }
  197. }