AddTempTargetForm.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. @State private var didPressSave =
  13. false // only used for fixing the Disclaimer showing up after pressing save (after the state was resetted), maybe refactor this...
  14. @State private var shouldDisplayHint = false
  15. @State var hintDetent = PresentationDetent.large
  16. @State var selectedVerboseHint: String?
  17. @State var hintLabel: String?
  18. var color: LinearGradient {
  19. colorScheme == .dark ? LinearGradient(
  20. gradient: Gradient(colors: [
  21. Color.bgDarkBlue,
  22. Color.bgDarkerDarkBlue
  23. ]),
  24. startPoint: .top,
  25. endPoint: .bottom
  26. )
  27. :
  28. LinearGradient(
  29. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  30. startPoint: .top,
  31. endPoint: .bottom
  32. )
  33. }
  34. private var formatter: NumberFormatter {
  35. let formatter = NumberFormatter()
  36. formatter.numberStyle = .decimal
  37. formatter.maximumFractionDigits = 0
  38. return formatter
  39. }
  40. private var glucoseFormatter: NumberFormatter {
  41. let formatter = NumberFormatter()
  42. formatter.numberStyle = .decimal
  43. formatter.maximumFractionDigits = 0
  44. if state.units == .mmolL {
  45. formatter.maximumFractionDigits = 1
  46. }
  47. formatter.roundingMode = .halfUp
  48. return formatter
  49. }
  50. var sliderEnabled: Bool {
  51. state.computeSliderHigh() > state.computeSliderLow()
  52. }
  53. var body: some View {
  54. NavigationView {
  55. Form {
  56. addTempTarget()
  57. }.scrollContentBackground(.hidden).background(color)
  58. .navigationTitle("Add Temp Target")
  59. .navigationBarTitleDisplayMode(.inline)
  60. .navigationBarItems(leading: Button("Close") {
  61. presentationMode.wrappedValue.dismiss()
  62. })
  63. .alert(
  64. "Start Temp Target",
  65. isPresented: $showAlert,
  66. actions: {
  67. Button("Cancel", role: .cancel) { state.isTempTargetEnabled = false }
  68. Button("Start Temp Target", role: .destructive) {
  69. Task {
  70. didPressSave.toggle()
  71. await setupAlertString()
  72. state.isTempTargetEnabled.toggle()
  73. await state.saveCustomTempTarget()
  74. await state.resetTempTargetState()
  75. dismiss()
  76. }
  77. }
  78. },
  79. message: {
  80. Text(alertString)
  81. }
  82. )
  83. .sheet(isPresented: $shouldDisplayHint) {
  84. SettingInputHintView(
  85. hintDetent: $hintDetent,
  86. shouldDisplayHint: $shouldDisplayHint,
  87. hintLabel: hintLabel ?? "",
  88. hintText: selectedVerboseHint ?? "",
  89. sheetTitle: "Help"
  90. )
  91. }
  92. }
  93. }
  94. @ViewBuilder private func addTempTarget() -> some View {
  95. Section(
  96. header: Text("Configure Temp Target"),
  97. content: {
  98. HStack {
  99. Text("Name")
  100. Spacer()
  101. TextField("Enter Name (optional)", text: $state.tempTargetName)
  102. .multilineTextAlignment(.trailing)
  103. }
  104. HStack {
  105. Text("Target")
  106. Spacer()
  107. TextFieldWithToolBar(text: $state.tempTargetTarget, placeholder: "0", numberFormatter: glucoseFormatter)
  108. .onChange(of: state.tempTargetTarget) { _ in
  109. // Recalculate the percentage when tempTargetTarget changes
  110. state.percentage = Double(state.computePercentage() * 100)
  111. }
  112. Text(state.units.rawValue).foregroundColor(.secondary)
  113. }
  114. HStack {
  115. Text("Duration")
  116. Spacer()
  117. TextFieldWithToolBar(text: $state.tempTargetDuration, placeholder: "0", numberFormatter: formatter)
  118. Text("minutes").foregroundColor(.secondary)
  119. }
  120. DatePicker("Date", selection: $state.date)
  121. }
  122. ).listRowBackground(Color.chart)
  123. if sliderEnabled && state.tempTargetTarget != 0 {
  124. if state.tempTargetTarget > 100 {
  125. Section {
  126. VStack {
  127. HStack(alignment: .top) {
  128. Text(
  129. "The current high Temp Target of \(state.tempTargetTarget) would raise your sensitivity to \(round(state.percentage)) % Insulin."
  130. )
  131. .font(.footnote)
  132. .foregroundColor(.secondary)
  133. .lineLimit(nil)
  134. Spacer()
  135. Button(
  136. action: {
  137. hintLabel = "Adjust Sensitivity for high Temp Target "
  138. selectedVerboseHint =
  139. "You have enabled High TempTarget Raises Sensitivity in your Target Behaviour setting. Therefore current high Temp Target of \(state.tempTargetTarget) would raise your sensitivity, therefore reduce Insulin dosing to \(round(state.percentage)) % of regular amount. This can be adjusted to another desired Insulin percentage!"
  140. shouldDisplayHint.toggle()
  141. },
  142. label: {
  143. HStack {
  144. Image(systemName: "questionmark.circle")
  145. }
  146. }
  147. ).buttonStyle(BorderlessButtonStyle())
  148. }.padding(.top)
  149. Toggle("Adjust sensitivity change for Temp Target?", isOn: $state.adjustSens).padding(.top)
  150. }.padding(.bottom)
  151. }.listRowBackground(Color.chart)
  152. } else if state.tempTargetTarget < 100 {
  153. Section {
  154. VStack {
  155. HStack(alignment: .top) {
  156. Text(
  157. "The current low Temp Target of \(state.tempTargetTarget) would lower your sensitivity to \(round(state.percentage)) % Insulin."
  158. )
  159. .font(.footnote)
  160. .foregroundColor(.secondary)
  161. .lineLimit(nil)
  162. Spacer()
  163. Button(
  164. action: {
  165. hintLabel = "Adjust Sensitivity for low Temp Target "
  166. selectedVerboseHint =
  167. "You have enabled Low TempTarget Lowers Sensitivity and autosens Max >1 in your Target Behaviour setting. Therefore current low Temp Target of \(state.tempTargetTarget) would lower your sensitivity, therefore increase Insulin dosing to \(round(state.percentage)) % of regular amount. This can be adjusted to another desired Insulin percentage!"
  168. shouldDisplayHint.toggle()
  169. },
  170. label: {
  171. HStack {
  172. Image(systemName: "questionmark.circle")
  173. }
  174. }
  175. ).buttonStyle(BorderlessButtonStyle())
  176. }.padding(.top)
  177. Toggle("Adjust sensitivity change for Temp Target?", isOn: $state.adjustSens).padding(.top)
  178. }.padding(.bottom)
  179. }.listRowBackground(Color.chart)
  180. }
  181. if state.adjustSens && state.tempTargetTarget != 100 {
  182. Section {
  183. VStack {
  184. // Display the percentage in large text
  185. Text("\(Int(state.percentage)) % Insulin")
  186. .foregroundColor(isUsingSlider ? .orange : Color.tabBar)
  187. .font(.largeTitle)
  188. // Bind the slider to the percentage
  189. Slider(
  190. value: $state.percentage,
  191. in: state.computeSliderLow() ... state.computeSliderHigh(),
  192. step: 5
  193. ) {} minimumValueLabel: {
  194. Text("\(state.computeSliderLow(), specifier: "%.0f")%")
  195. } maximumValueLabel: {
  196. Text("\(state.computeSliderHigh(), specifier: "%.0f")%")
  197. } onEditingChanged: { editing in
  198. isUsingSlider = editing
  199. state.halfBasalTarget = Decimal(state.computeHalfBasalTarget())
  200. }
  201. .disabled(!sliderEnabled)
  202. Divider()
  203. Text(
  204. state
  205. .units == .mgdL ?
  206. "Half Basal Exercise Target at: \(state.computeHalfBasalTarget().formatted(.number.precision(.fractionLength(0)))) mg/dl" :
  207. "Half Basal Exercise Target at: \(state.computeHalfBasalTarget().asMmolL.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))) mmol/L"
  208. )
  209. .foregroundColor(.secondary)
  210. .font(.caption).italic()
  211. }
  212. }.listRowBackground(Color.chart)
  213. }
  214. }
  215. // TODO: with iOS 17 we can change the body content wrapper from FORM to LIST and apply the .listSpacing modifier to make this all nice and small.
  216. Section {
  217. Button(action: {
  218. showAlert.toggle()
  219. }, label: {
  220. Text("Enact Temp Target")
  221. })
  222. .disabled(state.tempTargetDuration == 0)
  223. .frame(maxWidth: .infinity, alignment: .center)
  224. .tint(.white)
  225. }.listRowBackground(state.tempTargetDuration == 0 ? Color(.systemGray4) : Color(.systemBlue))
  226. Section {
  227. Button(action: {
  228. Task {
  229. didPressSave.toggle()
  230. await state.saveTempTargetPreset()
  231. dismiss()
  232. }
  233. }, label: {
  234. Text("Save as Preset")
  235. })
  236. .disabled(state.tempTargetDuration == 0)
  237. .frame(maxWidth: .infinity, alignment: .center)
  238. .tint(.white)
  239. }.listRowBackground(state.tempTargetDuration == 0 ? Color(.systemGray4) : Color(.orange))
  240. }
  241. private func setupAlertString() async {
  242. alertString =
  243. (
  244. state.tempTargetDuration > 0 ?
  245. (
  246. state
  247. .tempTargetDuration
  248. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
  249. " min."
  250. ) :
  251. NSLocalizedString(" infinite duration.", comment: "")
  252. ) +
  253. (
  254. state.tempTargetTarget == 0 ? "" :
  255. (" Target: " + state.tempTargetTarget.formatted() + " " + state.units.rawValue + ".")
  256. )
  257. +
  258. "\n\n"
  259. +
  260. NSLocalizedString(
  261. "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.",
  262. comment: ""
  263. )
  264. }
  265. }