AddTempTargetForm.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import Foundation
  2. import SwiftUI
  3. struct AddTempTargetForm: View {
  4. // settings for picker steps
  5. let smallMgdL = 1.0
  6. let bigMgdL = 5.0
  7. let smallMmolL = 0.1 / 0.0555
  8. let bigMmolL = 0.5 / 0.0555
  9. init(state: OverrideConfig.StateModel) {
  10. _state = StateObject(wrappedValue: state)
  11. _targetStep = State(initialValue: state.units == .mgdL ? bigMgdL : bigMmolL)
  12. }
  13. @State var toggleBigStepOn = true
  14. @StateObject var state: OverrideConfig.StateModel
  15. @Environment(\.presentationMode) var presentationMode
  16. @Environment(\.colorScheme) var colorScheme
  17. @Environment(\.dismiss) var dismiss
  18. @State private var targetStep: Double
  19. @State private var displayPickerTarget: Bool = false
  20. @State private var showAlert = false
  21. @State private var showPresetAlert = false
  22. @State private var alertString = ""
  23. @State private var isUsingSlider = false
  24. @State private var didPressSave =
  25. false // only used for fixing the Disclaimer showing up after pressing save (after the state was resetted), maybe refactor this...
  26. @State private var shouldDisplayHint = false
  27. @State var hintDetent = PresentationDetent.large
  28. @State var selectedVerboseHint: String?
  29. @State var hintLabel: String?
  30. var color: LinearGradient {
  31. colorScheme == .dark ? LinearGradient(
  32. gradient: Gradient(colors: [
  33. Color.bgDarkBlue,
  34. Color.bgDarkerDarkBlue
  35. ]),
  36. startPoint: .top,
  37. endPoint: .bottom
  38. )
  39. :
  40. LinearGradient(
  41. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  42. startPoint: .top,
  43. endPoint: .bottom
  44. )
  45. }
  46. private var formatter: NumberFormatter {
  47. let formatter = NumberFormatter()
  48. formatter.numberStyle = .decimal
  49. formatter.maximumFractionDigits = 0
  50. return formatter
  51. }
  52. private var glucoseFormatter: NumberFormatter {
  53. let formatter = NumberFormatter()
  54. formatter.numberStyle = .decimal
  55. formatter.maximumFractionDigits = 0
  56. if state.units == .mmolL {
  57. formatter.maximumFractionDigits = 1
  58. }
  59. formatter.roundingMode = .halfUp
  60. return formatter
  61. }
  62. var isSliderEnabled: Bool {
  63. state.computeSliderHigh() > state.computeSliderLow()
  64. }
  65. var body: some View {
  66. NavigationView {
  67. Form {
  68. addTempTarget()
  69. saveButton
  70. }.scrollContentBackground(.hidden).background(color)
  71. .navigationTitle("Add Temp Target")
  72. .navigationBarTitleDisplayMode(.inline)
  73. .navigationBarItems(leading: Button("Close") {
  74. presentationMode.wrappedValue.dismiss()
  75. })
  76. .sheet(isPresented: $shouldDisplayHint) {
  77. SettingInputHintView(
  78. hintDetent: $hintDetent,
  79. shouldDisplayHint: $shouldDisplayHint,
  80. hintLabel: hintLabel ?? "",
  81. hintText: selectedVerboseHint ?? "",
  82. sheetTitle: "Help"
  83. )
  84. }
  85. }
  86. }
  87. @ViewBuilder private func addTempTarget() -> some View {
  88. let pad: CGFloat = 3
  89. VStack {
  90. HStack {
  91. Text("Name")
  92. Spacer()
  93. TextField("(Optional)", text: $state.overrideName).multilineTextAlignment(.trailing)
  94. }
  95. .padding(.vertical, pad)
  96. }
  97. Section(
  98. header: Text("Configure Temp Target"),
  99. content: {
  100. HStack {
  101. Text("Name")
  102. Spacer()
  103. TextField("Enter Name (optional)", text: $state.tempTargetName)
  104. .multilineTextAlignment(.trailing)
  105. }
  106. HStack {
  107. Text("Duration")
  108. Spacer()
  109. TextFieldWithToolBar(text: $state.tempTargetDuration, placeholder: "0", numberFormatter: formatter)
  110. Text("minutes").foregroundColor(.secondary)
  111. }
  112. VStack {
  113. HStack {
  114. Text("Target Glucose")
  115. Spacer()
  116. Text(formattedGlucose(glucose: state.tempTargetTarget))
  117. .foregroundColor(!displayPickerTarget ? .primary : .accentColor)
  118. }
  119. .padding(.vertical, pad)
  120. .onTapGesture {
  121. displayPickerTarget.toggle()
  122. }
  123. if displayPickerTarget {
  124. HStack {
  125. VStack(alignment: .leading) {
  126. // Toggle for step iteration
  127. VStack {
  128. Text(formattedGlucose(glucose: Decimal(state.units == .mgdL ? smallMgdL : smallMmolL)))
  129. .tag(Int(state.units == .mgdL ? smallMgdL : smallMmolL))
  130. .foregroundColor(toggleBigStepOn ? .primary : .tabBar)
  131. ZStack {
  132. Group {
  133. Capsule()
  134. .frame(width: 22, height: 40)
  135. .foregroundColor(Color.loopGray)
  136. ZStack {
  137. Circle()
  138. .frame(width: 20, height: 22)
  139. Image(systemName: toggleBigStepOn ? "forward.circle.fill" : "play.circle.fill")
  140. .foregroundStyle(Color.white, Color.tabBar)
  141. }
  142. .shadow(color: .black.opacity(0.14), radius: 4, x: 0, y: 2)
  143. .offset(y: toggleBigStepOn ? 9 : -9)
  144. .padding(12)
  145. }
  146. }
  147. .onTapGesture {
  148. // Toggling between small and big step
  149. toggleBigStepOn.toggle()
  150. targetStep = toggleBigStepOn ? (state.units == .mgdL ? bigMgdL : bigMmolL) :
  151. (state.units == .mgdL ? smallMgdL : smallMmolL)
  152. }
  153. Text(formattedGlucose(glucose: Decimal(state.units == .mgdL ? bigMgdL : bigMmolL)))
  154. .tag(Int(state.units == .mgdL ? bigMgdL : bigMmolL))
  155. .foregroundColor(toggleBigStepOn ? .tabBar : .primary)
  156. }
  157. .padding(.top, 10)
  158. }
  159. .frame(maxWidth: .infinity)
  160. Spacer()
  161. // Picker on the right side
  162. Picker(
  163. selection: Binding(
  164. get: { Int(truncating: state.tempTargetTarget as NSNumber) },
  165. set: { state.tempTargetTarget = Decimal($0) }
  166. ), label: Text("")
  167. ) {
  168. ForEach(
  169. Array(stride(from: 80, through: 270, by: targetStep)),
  170. id: \.self
  171. ) { glucoseTarget in
  172. Text(formattedGlucose(glucose: Decimal(glucoseTarget)))
  173. .tag(Int(glucoseTarget))
  174. }
  175. }
  176. .pickerStyle(WheelPickerStyle())
  177. .frame(maxWidth: .infinity)
  178. }
  179. .frame(maxWidth: .infinity)
  180. }
  181. DatePicker("Date", selection: $state.date)
  182. }
  183. }
  184. ).listRowBackground(Color.chart)
  185. if isSliderEnabled && state.tempTargetTarget != 0 {
  186. if state.tempTargetTarget > 100 {
  187. Section {
  188. VStack(alignment: .leading) {
  189. Text("Raised Sensitivity:")
  190. .font(.footnote)
  191. .fontWeight(.bold)
  192. Text("Insulin reduced to \(formattedPercentage(state.percentage))% of regular amount.")
  193. .font(.footnote)
  194. .lineLimit(1)
  195. }
  196. }.listRowBackground(Color.tabBar)
  197. Section {
  198. VStack {
  199. Toggle("Adjust Sensitivity", isOn: $state.didAdjustSens).padding(.top)
  200. HStack(alignment: .top) {
  201. Text(
  202. "Temp Target raises Sensitivity. Further adjust if desired!"
  203. )
  204. .font(.footnote)
  205. .foregroundColor(.secondary)
  206. .lineLimit(nil)
  207. Spacer()
  208. Button(
  209. action: {
  210. hintLabel = "Adjust Sensitivity for high Temp Target "
  211. selectedVerboseHint =
  212. "You have enabled High TempTarget Raises Sensitivity in Target Behaviour settings. Therefore current high Temp Target of \(state.tempTargetTarget) would raise your sensitivity, hence reduce Insulin dosing to \(formattedPercentage(state.percentage)) % of regular amount. This can be adjusted to another desired Insulin percentage!"
  213. shouldDisplayHint.toggle()
  214. },
  215. label: {
  216. HStack {
  217. Image(systemName: "questionmark.circle")
  218. }
  219. }
  220. ).buttonStyle(BorderlessButtonStyle())
  221. }.padding(.top)
  222. }.padding(.bottom)
  223. }.listRowBackground(Color.chart)
  224. } else if state.tempTargetTarget < 100 {
  225. Section {
  226. VStack(alignment: .leading) {
  227. Text("Lowered Sensitivity:")
  228. .font(.footnote)
  229. .fontWeight(.bold)
  230. Text("Insulin increased to \(formattedPercentage(state.percentage))% of regular amount.")
  231. .font(.footnote)
  232. .lineLimit(1)
  233. }
  234. }.listRowBackground(Color.tabBar)
  235. Section {
  236. VStack {
  237. Toggle("Adjust Insulin %", isOn: $state.didAdjustSens).padding(.top)
  238. HStack(alignment: .top) {
  239. Text(
  240. "Temp Target lowers Sensitivity. Further adjust if desired!"
  241. )
  242. .font(.footnote)
  243. .foregroundColor(.secondary)
  244. .lineLimit(nil)
  245. Spacer()
  246. Button(
  247. action: {
  248. hintLabel = "Adjust Sensitivity for low Temp Target "
  249. selectedVerboseHint =
  250. "You have enabled Low TempTarget Lowers Sensitivity in Target Behaviour settings and set autosens Max > 1. Therefore current low Temp Target of \(state.tempTargetTarget) would lower your sensitivity, hence increase Insulin dosing to \(formattedPercentage(state.percentage)) % of regular amount. This can be adjusted to another desired Insulin percentage!"
  251. shouldDisplayHint.toggle()
  252. },
  253. label: {
  254. HStack {
  255. Image(systemName: "questionmark.circle")
  256. }
  257. }
  258. ).buttonStyle(BorderlessButtonStyle())
  259. }.padding(.top)
  260. }.padding(.bottom)
  261. }.listRowBackground(Color.chart)
  262. }
  263. if state.didAdjustSens && state.tempTargetTarget != 100 {
  264. Section {
  265. VStack {
  266. Text("\(Int(state.percentage)) % Insulin")
  267. .foregroundColor(isUsingSlider ? .orange : Color.tabBar)
  268. .font(.largeTitle)
  269. Slider(
  270. value: $state.percentage,
  271. in: state.computeSliderLow() ... state.computeSliderHigh(),
  272. step: 5
  273. ) {} minimumValueLabel: {
  274. Text("\(state.computeSliderLow(), specifier: "%.0f")%")
  275. } maximumValueLabel: {
  276. Text("\(state.computeSliderHigh(), specifier: "%.0f")%")
  277. } onEditingChanged: { editing in
  278. isUsingSlider = editing
  279. state.halfBasalTarget = Decimal(state.computeHalfBasalTarget())
  280. }
  281. .disabled(!isSliderEnabled)
  282. Divider()
  283. HStack {
  284. Text(
  285. "Half Basal Exercise Target at: \(formattedGlucose(glucose: Decimal(state.computeHalfBasalTarget())))"
  286. )
  287. .lineLimit(1)
  288. .minimumScaleFactor(0.5)
  289. .foregroundColor(.secondary)
  290. Spacer()
  291. }
  292. }
  293. }.listRowBackground(Color.chart)
  294. }
  295. }
  296. }
  297. private func isTempTargetInvalid() -> (Bool, String?) {
  298. let noDurationSpecified = state.tempTargetDuration == 0
  299. let targetZero = state.tempTargetTarget < 80
  300. if noDurationSpecified {
  301. return (true, "Set a duration!")
  302. }
  303. if targetZero {
  304. return (
  305. true,
  306. "\(state.units == .mgdL ? "80 " : "4.4 ")" + state.units.rawValue + " needed as min. Glucose Target!"
  307. )
  308. }
  309. return (false, nil)
  310. }
  311. private var saveButton: some View {
  312. let (isInvalid, errorMessage) = isTempTargetInvalid()
  313. let noNameSpecified = state.tempTargetName == ""
  314. return Group {
  315. Section(
  316. header:
  317. HStack {
  318. Spacer()
  319. Text(errorMessage ?? "").textCase(nil)
  320. .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
  321. Spacer()
  322. },
  323. content: {
  324. Button(action: {
  325. Task {
  326. if noNameSpecified { state.tempTargetName = "Custom Target" }
  327. didPressSave.toggle()
  328. state.isTempTargetEnabled.toggle()
  329. await state.saveCustomTempTarget()
  330. await state.resetTempTargetState()
  331. dismiss()
  332. }
  333. }, label: {
  334. Text("Enact Temp Target")
  335. })
  336. .disabled(isInvalid)
  337. .frame(maxWidth: .infinity, alignment: .center)
  338. .tint(.white)
  339. }
  340. ).listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
  341. Section {
  342. Button(action: {
  343. Task {
  344. if noNameSpecified { state.tempTargetName = "Custom Target" }
  345. didPressSave.toggle()
  346. await state.saveTempTargetPreset()
  347. dismiss()
  348. }
  349. }, label: {
  350. Text("Save as Preset")
  351. })
  352. .disabled(isInvalid)
  353. .frame(maxWidth: .infinity, alignment: .center)
  354. .tint(.white)
  355. }
  356. .listRowBackground(
  357. isInvalid ? Color(.systemGray4) : Color.secondary
  358. )
  359. }
  360. }
  361. private func formattedPercentage(_ value: Double) -> String {
  362. let percentageNumber = NSNumber(value: value)
  363. return formatter.string(from: percentageNumber) ?? "\(value)"
  364. }
  365. private func formattedGlucose(glucose: Decimal) -> String {
  366. let formattedValue: String
  367. if state.units == .mgdL {
  368. formattedValue = glucoseFormatter.string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
  369. } else {
  370. formattedValue = glucose.formattedAsMmolL
  371. }
  372. return "\(formattedValue) \(state.units.rawValue)"
  373. }
  374. }