AddOverrideForm.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import Foundation
  2. import SwiftUI
  3. struct AddOverrideForm: View {
  4. @Environment(\.presentationMode) var presentationMode
  5. @Bindable var state: OverrideConfig.StateModel
  6. @State private var isEditing = false
  7. @State private var overrideTarget = false
  8. @Environment(\.colorScheme) var colorScheme
  9. @State private var showAlert = false
  10. @State private var alertString = ""
  11. @Environment(\.dismiss) var dismiss
  12. @Environment(AppState.self) var appState
  13. private var formatter: NumberFormatter {
  14. let formatter = NumberFormatter()
  15. formatter.numberStyle = .decimal
  16. formatter.maximumFractionDigits = 0
  17. return formatter
  18. }
  19. private var glucoseFormatter: NumberFormatter {
  20. let formatter = NumberFormatter()
  21. formatter.numberStyle = .decimal
  22. formatter.maximumFractionDigits = 0
  23. if state.units == .mmolL {
  24. formatter.maximumFractionDigits = 1
  25. }
  26. formatter.roundingMode = .halfUp
  27. return formatter
  28. }
  29. private var alertMessage: String {
  30. let target: String = state.units == .mgdL ? "70-270 mg/dl" : "4-15 mmol/l"
  31. return "Please enter a valid target between" + " \(target)."
  32. }
  33. var body: some View {
  34. NavigationView {
  35. Form {
  36. addOverride()
  37. }.scrollContentBackground(.hidden)
  38. .background(appState.trioBackgroundColor(for: colorScheme))
  39. .navigationTitle("Add Override")
  40. .navigationBarItems(trailing: Button("Cancel") {
  41. presentationMode.wrappedValue.dismiss()
  42. })
  43. }
  44. }
  45. @ViewBuilder private func addOverride() -> some View {
  46. Section {
  47. VStack {
  48. TextField("Name", text: $state.overrideName)
  49. }
  50. } header: {
  51. Text("Name")
  52. }.listRowBackground(Color.chart)
  53. Section {
  54. VStack {
  55. Spacer()
  56. Text("\(state.overrideSliderPercentage.formatted(.number)) %")
  57. .foregroundColor(
  58. state
  59. .overrideSliderPercentage >= 130 ? .red :
  60. (isEditing ? .orange : Color.tabBar)
  61. )
  62. .font(.largeTitle)
  63. Slider(
  64. value: $state.overrideSliderPercentage,
  65. in: 10 ... 200,
  66. step: 1,
  67. onEditingChanged: { editing in
  68. isEditing = editing
  69. }
  70. )
  71. Spacer()
  72. Toggle(isOn: $state.indefinite) {
  73. Text("Enable indefinitely")
  74. }
  75. }
  76. if !state.indefinite {
  77. HStack {
  78. Text("Duration")
  79. TextFieldWithToolBar(text: $state.overrideDuration, placeholder: "0", numberFormatter: formatter)
  80. Text("minutes").foregroundColor(.secondary)
  81. }
  82. }
  83. HStack {
  84. Toggle(isOn: $state.shouldOverrideTarget) {
  85. Text("Override Profile Target")
  86. }
  87. }
  88. if state.shouldOverrideTarget {
  89. HStack {
  90. Text("Target Glucose")
  91. TextFieldWithToolBar(text: $state.target, placeholder: "0", numberFormatter: glucoseFormatter)
  92. Text(state.units.rawValue).foregroundColor(.secondary)
  93. }
  94. }
  95. HStack {
  96. Toggle(isOn: $state.advancedSettings) {
  97. Text("More options")
  98. }
  99. }
  100. if state.advancedSettings {
  101. HStack {
  102. Toggle(isOn: $state.smbIsOff) {
  103. Text("Disable SMBs")
  104. }
  105. }
  106. HStack {
  107. Toggle(isOn: $state.smbIsAlwaysOff) {
  108. Text("Schedule when SMBs are Off")
  109. }.disabled(!state.smbIsOff)
  110. }
  111. if state.smbIsAlwaysOff {
  112. HStack {
  113. Text("First Hour SMBs are Off (24 hours)")
  114. TextFieldWithToolBar(text: $state.start, placeholder: "0", numberFormatter: formatter)
  115. Text("hour").foregroundColor(.secondary)
  116. }
  117. HStack {
  118. Text("Last Hour SMBs are Off (24 hours)")
  119. TextFieldWithToolBar(text: $state.end, placeholder: "0", numberFormatter: formatter)
  120. Text("hour").foregroundColor(.secondary)
  121. }
  122. }
  123. HStack {
  124. Toggle(isOn: $state.isfAndCr) {
  125. Text("Change ISF and CR")
  126. }
  127. }
  128. if !state.isfAndCr {
  129. HStack {
  130. Toggle(isOn: $state.isf) {
  131. Text("Change ISF")
  132. }
  133. }
  134. HStack {
  135. Toggle(isOn: $state.cr) {
  136. Text("Change CR")
  137. }
  138. }
  139. }
  140. HStack {
  141. Text("SMB Minutes")
  142. TextFieldWithToolBar(text: $state.smbMinutes, placeholder: "0", numberFormatter: formatter)
  143. Text("minutes").foregroundColor(.secondary)
  144. }
  145. HStack {
  146. Text("UAM SMB Minutes")
  147. TextFieldWithToolBar(text: $state.uamMinutes, placeholder: "0", numberFormatter: formatter)
  148. Text("minutes").foregroundColor(.secondary)
  149. }
  150. }
  151. startAndSaveProfiles
  152. }
  153. header: { Text("Add custom Override") }
  154. footer: {
  155. Text(
  156. "Your profile basal insulin will be adjusted with the override percentage and your profile ISF and CR will be inversly adjusted with the percentage."
  157. )
  158. }.listRowBackground(Color.chart)
  159. }
  160. private var startAndSaveProfiles: some View {
  161. HStack {
  162. Button("Start new Override") {
  163. if !state.isInputInvalid(target: state.target) {
  164. showAlert.toggle()
  165. alertString = "\(state.overrideSliderPercentage.formatted(.number)) %, " +
  166. (
  167. state.overrideDuration > 0 || !state
  168. .indefinite ?
  169. (
  170. state
  171. .overrideDuration
  172. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
  173. " min."
  174. ) :
  175. NSLocalizedString(" infinite duration.", comment: "")
  176. ) +
  177. (
  178. (state.target == 0 || !state.shouldOverrideTarget) ? "" :
  179. (" Target: " + state.target.formatted() + " " + state.units.rawValue + ".")
  180. )
  181. +
  182. (
  183. state
  184. .smbIsOff ?
  185. NSLocalizedString(
  186. " SMBs are disabled either by schedule or during the entire duration.",
  187. comment: ""
  188. ) : ""
  189. )
  190. +
  191. "\n\n"
  192. +
  193. NSLocalizedString(
  194. "Starting this override will change your profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Override” will start your new Override or edit your current active Override.",
  195. comment: ""
  196. )
  197. }
  198. }
  199. .disabled(unChanged())
  200. .buttonStyle(BorderlessButtonStyle())
  201. .font(.callout)
  202. .controlSize(.mini)
  203. .alert(
  204. "Start Override",
  205. isPresented: $showAlert,
  206. actions: {
  207. Button("Cancel", role: .cancel) { state.isEnabled = false }
  208. Button("Start Override", role: .destructive) {
  209. Task {
  210. if state.indefinite { state.overrideDuration = 0 }
  211. state.isEnabled.toggle()
  212. await state.saveCustomOverride()
  213. await state.resetStateVariables()
  214. dismiss()
  215. }
  216. }
  217. },
  218. message: {
  219. Text(alertString)
  220. }
  221. )
  222. .alert(isPresented: $state.showInvalidTargetAlert) {
  223. Alert(
  224. title: Text("Invalid Input"),
  225. message: Text("\(state.alertMessage)"),
  226. dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
  227. )
  228. }
  229. Button {
  230. Task {
  231. if !state.isInputInvalid(target: state.target) {
  232. await state.saveOverridePreset()
  233. dismiss()
  234. }
  235. }
  236. }
  237. label: { Text("Save as Preset") }
  238. .tint(.orange)
  239. .frame(maxWidth: .infinity, alignment: .trailing)
  240. .buttonStyle(BorderlessButtonStyle())
  241. .controlSize(.mini)
  242. .disabled(unChanged())
  243. }
  244. }
  245. private func unChanged() -> Bool {
  246. let isChanged = (
  247. state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.smbIsOff && !state
  248. .advancedSettings
  249. ) ||
  250. (!state.indefinite && state.overrideDuration == 0) || (state.shouldOverrideTarget && state.target == 0) ||
  251. (
  252. state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.smbIsOff && state.isf && state
  253. .cr && state
  254. .smbMinutes == state.defaultSmbMinutes && state.uamMinutes == state.defaultUamMinutes
  255. )
  256. return isChanged
  257. }
  258. }