UserInterfaceSettingsRootView.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. import SwiftUI
  2. import Swinject
  3. extension UserInterfaceSettings {
  4. struct RootView: BaseView {
  5. let resolver: Resolver
  6. @StateObject var state = StateModel()
  7. @State private var shouldDisplayHint: Bool = false
  8. @State var hintDetent = PresentationDetent.large
  9. @State var selectedVerboseHint: AnyView?
  10. @State var hintLabel: String?
  11. @State private var decimalPlaceholder: Decimal = 0.0
  12. @State private var booleanPlaceholder: Bool = false
  13. @State private var displayPickerLowThreshold: Bool = false
  14. @State private var displayPickerHighThreshold: Bool = false
  15. @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
  16. @Environment(\.colorScheme) var colorScheme
  17. var color: LinearGradient {
  18. colorScheme == .dark ? LinearGradient(
  19. gradient: Gradient(colors: [
  20. Color.bgDarkBlue,
  21. Color.bgDarkerDarkBlue
  22. ]),
  23. startPoint: .top,
  24. endPoint: .bottom
  25. )
  26. :
  27. LinearGradient(
  28. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  29. startPoint: .top,
  30. endPoint: .bottom
  31. )
  32. }
  33. private var glucoseFormatter: NumberFormatter {
  34. let formatter = NumberFormatter()
  35. formatter.numberStyle = .decimal
  36. formatter.maximumFractionDigits = 0
  37. if state.units == .mmolL {
  38. formatter.maximumFractionDigits = 1
  39. }
  40. formatter.roundingMode = .halfUp
  41. return formatter
  42. }
  43. private var carbsFormatter: NumberFormatter {
  44. let formatter = NumberFormatter()
  45. formatter.numberStyle = .decimal
  46. formatter.maximumFractionDigits = 0
  47. return formatter
  48. }
  49. var body: some View {
  50. Form {
  51. Section(
  52. header: Text("General Appearance"),
  53. content: {
  54. VStack {
  55. Picker(
  56. selection: $colorSchemePreference,
  57. label: Text("Trio Color Scheme")
  58. ) {
  59. ForEach(ColorSchemeOption.allCases) { selection in
  60. Text(selection.displayName).tag(selection)
  61. }
  62. }.padding(.top)
  63. HStack(alignment: .top) {
  64. Text(
  65. "Choose between Light, Dark, or System Default for the app color scheme"
  66. )
  67. .font(.footnote)
  68. .foregroundColor(.secondary)
  69. .lineLimit(nil)
  70. Spacer()
  71. Button(
  72. action: {
  73. hintLabel = "Color Scheme Preference"
  74. selectedVerboseHint =
  75. AnyView(Text("Set the app color scheme using the following options \n\nSystem Default: Follows the phone's current color scheme setting at that time\nLight: Always in Light mode \nDark: Always in Dark mode"))
  76. shouldDisplayHint.toggle()
  77. },
  78. label: {
  79. HStack {
  80. Image(systemName: "questionmark.circle")
  81. }
  82. }
  83. ).buttonStyle(BorderlessButtonStyle())
  84. }.padding(.top)
  85. }.padding(.bottom)
  86. }
  87. ).listRowBackground(Color.chart)
  88. Section {
  89. VStack {
  90. Picker(
  91. selection: $state.glucoseColorScheme,
  92. label: Text("Glucose Color Scheme")
  93. ) {
  94. ForEach(GlucoseColorScheme.allCases) { selection in
  95. Text(selection.displayName).tag(selection)
  96. }
  97. }.padding(.top)
  98. HStack(alignment: .top) {
  99. Text(
  100. "Choose between Static or Dynamic coloring for glucose readings"
  101. )
  102. .font(.footnote)
  103. .foregroundColor(.secondary)
  104. .lineLimit(nil)
  105. Spacer()
  106. Button(
  107. action: {
  108. hintLabel = "Glucose Color Scheme"
  109. selectedVerboseHint =
  110. AnyView(
  111. Text(
  112. "Set the color scheme for glucose readings on the main glucose graph, live activities, and bolus calculator using the following options: \n\nStatic: Below-Range Target readings will be in RED, In-Range will be GREEN, Above-Range will be YELLOW \n\nDynamic: Readings on Target will be GREEN. As readings approach and exceed below target, they become more RED. As readings approach and exceed above targer, they become more PURPLE."
  113. )
  114. )
  115. shouldDisplayHint.toggle()
  116. },
  117. label: {
  118. HStack {
  119. Image(systemName: "questionmark.circle")
  120. }
  121. }
  122. ).buttonStyle(BorderlessButtonStyle())
  123. }.padding(.top)
  124. }.padding(.bottom)
  125. }.listRowBackground(Color.chart)
  126. Section(
  127. header: Text("Home View Settings"),
  128. content: {
  129. VStack {
  130. Toggle("Show X-Axis Grid Lines", isOn: $state.xGridLines)
  131. Toggle("Show Y-Axis Grid Lines", isOn: $state.yGridLines)
  132. HStack(alignment: .top) {
  133. Text(
  134. "Display the grid lines behind the glucose graph"
  135. )
  136. .font(.footnote)
  137. .foregroundColor(.secondary)
  138. .lineLimit(nil)
  139. Spacer()
  140. Button(
  141. action: {
  142. hintLabel = "Show Main Chart X- and Y-Axis Grid Lines"
  143. selectedVerboseHint =
  144. AnyView(Text("Choose whether or not to display one or both X- and Y-Axis grid lines."))
  145. shouldDisplayHint.toggle()
  146. },
  147. label: {
  148. HStack {
  149. Image(systemName: "questionmark.circle")
  150. }
  151. }
  152. ).buttonStyle(BorderlessButtonStyle())
  153. }.padding(.top)
  154. }.padding(.vertical)
  155. }
  156. ).listRowBackground(Color.chart)
  157. SettingInputSection(
  158. decimalValue: $decimalPlaceholder,
  159. booleanValue: $state.rulerMarks,
  160. shouldDisplayHint: $shouldDisplayHint,
  161. selectedVerboseHint: Binding(
  162. get: { selectedVerboseHint },
  163. set: {
  164. selectedVerboseHint = $0.map { AnyView($0) }
  165. hintLabel = "Show Low and High Thresholds"
  166. }
  167. ),
  168. units: state.units,
  169. type: .boolean,
  170. label: "Show Low and High Thresholds",
  171. miniHint: "Display the Low and High glucose thresholds set below",
  172. verboseHint: Text("This setting displays the upper and lower values for your glucose target range. \n\nThis range is for display and statistical purposes only and does not influence insulin dosing.")
  173. )
  174. if state.rulerMarks {
  175. Section {
  176. VStack {
  177. VStack {
  178. HStack {
  179. Text("Low Threshold")
  180. Spacer()
  181. Group {
  182. Text(state.units == .mgdL ? state.low.description : state.low.asMmolL.description)
  183. .foregroundColor(!displayPickerLowThreshold ? .primary : .accentColor)
  184. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  185. }
  186. }
  187. .onTapGesture {
  188. displayPickerLowThreshold.toggle()
  189. }
  190. }
  191. .padding(.top)
  192. if displayPickerLowThreshold {
  193. let setting = PickerSettingsProvider.shared.settings.low
  194. Picker(selection: $state.low, label: Text("")) {
  195. ForEach(
  196. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  197. id: \.self
  198. ) { value in
  199. let displayValue = state.units == .mgdL ? value : value.asMmolL
  200. Text("\(displayValue.description)").tag(value)
  201. }
  202. }
  203. .pickerStyle(WheelPickerStyle())
  204. .frame(maxWidth: .infinity)
  205. }
  206. VStack {
  207. HStack {
  208. Text("High Threshold")
  209. Spacer()
  210. Group {
  211. Text(state.units == .mgdL ? state.high.description : state.high.asMmolL.description)
  212. .foregroundColor(!displayPickerHighThreshold ? .primary : .accentColor)
  213. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  214. }
  215. }
  216. .onTapGesture {
  217. displayPickerHighThreshold.toggle()
  218. }
  219. }
  220. .padding(.top)
  221. if displayPickerHighThreshold {
  222. let setting = PickerSettingsProvider.shared.settings.high
  223. Picker(selection: $state.high, label: Text("")) {
  224. ForEach(
  225. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  226. id: \.self
  227. ) { value in
  228. let displayValue = state.units == .mgdL ? value : value.asMmolL
  229. Text("\(displayValue.description)").tag(value)
  230. }
  231. }
  232. .pickerStyle(WheelPickerStyle())
  233. .frame(maxWidth: .infinity)
  234. }
  235. HStack(alignment: .top) {
  236. Text(
  237. "Set low and high glucose values for the main screen glucose graph and statistics \nLow Default: 70 \nHigh Default: 180"
  238. )
  239. .lineLimit(nil)
  240. .font(.footnote)
  241. .foregroundColor(.secondary)
  242. Spacer()
  243. Button(
  244. action: {
  245. hintLabel = "Low and High Thresholds"
  246. selectedVerboseHint =
  247. AnyView(Text("Default values are based on internationally accepted Time in Range values of 70-180 mg/dL (5.5-10 mmol/L) \nSet the values used in the main screen glucose graph and to determine Time in Range for Statistics. \nNote: These values are not used to calculate insulin dosing."))
  248. shouldDisplayHint.toggle()
  249. },
  250. label: {
  251. HStack {
  252. Image(systemName: "questionmark.circle")
  253. }
  254. }
  255. ).buttonStyle(BorderlessButtonStyle())
  256. }.padding(.top)
  257. }.padding(.bottom)
  258. }.listRowBackground(Color.chart)
  259. }
  260. Section {
  261. VStack {
  262. Picker(
  263. selection: $state.forecastDisplayType,
  264. label: Text("Forecast Display Type")
  265. ) {
  266. ForEach(ForecastDisplayType.allCases) { selection in
  267. Text(selection.displayName).tag(selection)
  268. }
  269. }.padding(.top)
  270. HStack(alignment: .top) {
  271. Text(
  272. "Choose between the OpenAPS colored Lines or the Cone of Uncertainty for the Forecast Lines \nDefault: Cone"
  273. )
  274. .font(.footnote)
  275. .foregroundColor(.secondary)
  276. .lineLimit(nil)
  277. Spacer()
  278. Button(
  279. action: {
  280. hintLabel = "Forecast Display Type"
  281. selectedVerboseHint =
  282. AnyView(Text("This setting allows you to choose between the following two options for the Forecast lines (previously: Prediction Lines). \n\nLines: Uses the IOB, COB, UAM, and ZT forecast lines from OpenAPS \n\nCone: Uses a combined range of all possible forecasts from the OpenAPS lines and provides you with a range of possible forecasts. This option has shown to reduce confusion and stress around algorithm forecasts by providing a less concerning visual representation."))
  283. shouldDisplayHint.toggle()
  284. },
  285. label: {
  286. HStack {
  287. Image(systemName: "questionmark.circle")
  288. }
  289. }
  290. ).buttonStyle(BorderlessButtonStyle())
  291. }.padding(.top)
  292. }.padding(.bottom)
  293. }.listRowBackground(Color.chart)
  294. SettingInputSection(
  295. decimalValue: $state.hours,
  296. booleanValue: $booleanPlaceholder,
  297. shouldDisplayHint: $shouldDisplayHint,
  298. selectedVerboseHint: Binding(
  299. get: { selectedVerboseHint },
  300. set: {
  301. selectedVerboseHint = $0.map { AnyView($0) }
  302. hintLabel = "X-Axis Interval Step"
  303. }
  304. ),
  305. units: state.units,
  306. type: .decimal("hours"),
  307. label: "X-Axis Interval Step",
  308. miniHint: "Determines how many hours are shown in the main graph",
  309. verboseHint: Text("Default: 6 hours \n\nThis setting determines how many hours are shown in the primary view of the main graph.")
  310. )
  311. Section {
  312. VStack {
  313. Picker(
  314. selection: $state.totalInsulinDisplayType,
  315. label: Text("Total Insulin Display Type")
  316. ) {
  317. ForEach(TotalInsulinDisplayType.allCases) { selection in
  318. Text(selection.displayName).tag(selection)
  319. }
  320. }.padding(.top)
  321. HStack(alignment: .top) {
  322. Text(
  323. "Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph"
  324. )
  325. .font(.footnote)
  326. .foregroundColor(.secondary)
  327. .lineLimit(nil)
  328. Spacer()
  329. Button(
  330. action: {
  331. hintLabel = "Total Insulin Display Type"
  332. selectedVerboseHint =
  333. AnyView(Text("Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph.\n\nTotal Daily Dose: Displays the last 24 hours of total insulin administered, both basal and bolus. \n\nTotal Insulin in Scope: Displays the total insulin administered since midnight, both basal and bolus."))
  334. shouldDisplayHint.toggle()
  335. },
  336. label: {
  337. HStack {
  338. Image(systemName: "questionmark.circle")
  339. }
  340. }
  341. ).buttonStyle(BorderlessButtonStyle())
  342. }.padding(.top)
  343. }.padding(.bottom)
  344. }.listRowBackground(Color.chart)
  345. // TODO: this needs to be a picker: mmol/L or %
  346. SettingInputSection(
  347. decimalValue: $decimalPlaceholder,
  348. booleanValue: $state.overrideHbA1cUnit,
  349. shouldDisplayHint: $shouldDisplayHint,
  350. selectedVerboseHint: Binding(
  351. get: { selectedVerboseHint },
  352. set: {
  353. selectedVerboseHint = $0.map { AnyView($0) }
  354. hintLabel = "Override HbA1c Unit"
  355. }
  356. ),
  357. units: state.units,
  358. type: .boolean,
  359. label: "Override HbA1c Unit",
  360. miniHint: "Display HbA1c in mmol/mol or %",
  361. verboseHint: Text("Choose which format you'd prefer the HbA1c value in the statistics view as a percentage (Example: 6.5%) or mmol/mol (Example: 48 mmol/mol)"),
  362. headerText: "Trio Statistics"
  363. )
  364. // TODO: this needs to be a picker: choose bar chart or progress bar
  365. SettingInputSection(
  366. decimalValue: $decimalPlaceholder,
  367. booleanValue: $state.oneDimensionalGraph,
  368. shouldDisplayHint: $shouldDisplayHint,
  369. selectedVerboseHint: Binding(
  370. get: { selectedVerboseHint },
  371. set: {
  372. selectedVerboseHint = $0.map { AnyView($0) }
  373. hintLabel = "Standing / Laying TIR Chart"
  374. }
  375. ),
  376. units: state.units,
  377. type: .boolean,
  378. label: "Standing / Laying TIR Chart",
  379. miniHint: "Select a vertical chart or horizontal chart to display your Time in Range Statistics",
  380. verboseHint: Text("Select a vertical / standing chart by turning this feature OFF \n\nSelect a horizontal / laying chart by turning this feature ON")
  381. )
  382. SettingInputSection(
  383. decimalValue: $state.carbsRequiredThreshold,
  384. booleanValue: $state.showCarbsRequiredBadge,
  385. shouldDisplayHint: $shouldDisplayHint,
  386. selectedVerboseHint: Binding(
  387. get: { selectedVerboseHint },
  388. set: {
  389. selectedVerboseHint = $0.map { AnyView($0) }
  390. hintLabel = "Show Carbs Required Badge"
  391. }
  392. ),
  393. units: state.units,
  394. type: .conditionalDecimal("carbsRequiredThreshold"),
  395. label: "Show Carbs Required Badge",
  396. conditionalLabel: "Carbs Required Threshold",
  397. miniHint: "Show carbs required as a notification badge on the home screen",
  398. verboseHint: Text("Turning this on will show the grams of carbs needed to prevent a low as a notification badge on the Trio home screen located above the main icon"),
  399. headerText: "Carbs Required Badge"
  400. )
  401. }
  402. .sheet(isPresented: $shouldDisplayHint) {
  403. SettingInputHintView(
  404. hintDetent: $hintDetent,
  405. shouldDisplayHint: $shouldDisplayHint,
  406. hintLabel: hintLabel ?? "",
  407. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  408. sheetTitle: "Help"
  409. )
  410. }
  411. .scrollContentBackground(.hidden).background(color)
  412. .onAppear(perform: configureView)
  413. .navigationBarTitle("User Interface")
  414. .navigationBarTitleDisplayMode(.automatic)
  415. }
  416. }
  417. }