UserInterfaceSettingsRootView.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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("""
  76. Set the app color scheme using the following options
  77. System Default: Follows the phone's current color scheme setting at that time
  78. Light: Always in Light mode
  79. Dark: Always in Dark mode
  80. """))
  81. shouldDisplayHint.toggle()
  82. },
  83. label: {
  84. HStack {
  85. Image(systemName: "questionmark.circle")
  86. }
  87. }
  88. ).buttonStyle(BorderlessButtonStyle())
  89. }.padding(.top)
  90. }.padding(.bottom)
  91. }
  92. ).listRowBackground(Color.chart)
  93. Section {
  94. VStack {
  95. Picker(
  96. selection: $state.glucoseColorScheme,
  97. label: Text("Glucose Color Scheme")
  98. ) {
  99. ForEach(GlucoseColorScheme.allCases) { selection in
  100. Text(selection.displayName).tag(selection)
  101. }
  102. }.padding(.top)
  103. HStack(alignment: .top) {
  104. Text(
  105. "Choose between Static or Dynamic coloring for glucose readings"
  106. )
  107. .font(.footnote)
  108. .foregroundColor(.secondary)
  109. .lineLimit(nil)
  110. Spacer()
  111. Button(
  112. action: {
  113. hintLabel = "Glucose Color Scheme"
  114. selectedVerboseHint =
  115. AnyView(
  116. Text("""
  117. Set the color scheme for glucose readings on the main glucose graph, live activities, and bolus calculatorusing the following options:
  118. Static: Below-Range Target readings will be in RED, In-Range will be GREEN, Above-Range will be YELLOW
  119. Dynamic: 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.
  120. """
  121. )
  122. )
  123. shouldDisplayHint.toggle()
  124. },
  125. label: {
  126. HStack {
  127. Image(systemName: "questionmark.circle")
  128. }
  129. }
  130. ).buttonStyle(BorderlessButtonStyle())
  131. }.padding(.top)
  132. }.padding(.bottom)
  133. }.listRowBackground(Color.chart)
  134. Section(
  135. header: Text("Home View Settings"),
  136. content: {
  137. VStack {
  138. Toggle("Show X-Axis Grid Lines", isOn: $state.xGridLines)
  139. Toggle("Show Y-Axis Grid Lines", isOn: $state.yGridLines)
  140. HStack(alignment: .top) {
  141. Text(
  142. "Display the grid lines behind the glucose graph"
  143. )
  144. .font(.footnote)
  145. .foregroundColor(.secondary)
  146. .lineLimit(nil)
  147. Spacer()
  148. Button(
  149. action: {
  150. hintLabel = "Show Main Chart X- and Y-Axis Grid Lines"
  151. selectedVerboseHint =
  152. AnyView(Text("""
  153. Choose whether or not to display one or both X- and Y-Axis grid lines.
  154. """))
  155. shouldDisplayHint.toggle()
  156. },
  157. label: {
  158. HStack {
  159. Image(systemName: "questionmark.circle")
  160. }
  161. }
  162. ).buttonStyle(BorderlessButtonStyle())
  163. }.padding(.top)
  164. }.padding(.vertical)
  165. }
  166. ).listRowBackground(Color.chart)
  167. SettingInputSection(
  168. decimalValue: $decimalPlaceholder,
  169. booleanValue: $state.rulerMarks,
  170. shouldDisplayHint: $shouldDisplayHint,
  171. selectedVerboseHint: Binding(
  172. get: { selectedVerboseHint },
  173. set: {
  174. selectedVerboseHint = $0.map { AnyView($0) }
  175. hintLabel = "Show Low and High Thresholds"
  176. }
  177. ),
  178. units: state.units,
  179. type: .boolean,
  180. label: "Show Low and High Thresholds",
  181. miniHint: "Display the Low and High glucose Thresholds set below",
  182. verboseHint: Text("""
  183. This setting displays the upper and lower values for your glucose target range.
  184. This range is for display and statistical purposes only and does not influence insulin dosing.
  185. """)
  186. )
  187. if state.rulerMarks {
  188. Section {
  189. VStack {
  190. VStack {
  191. HStack {
  192. Text("Low Threshold")
  193. Spacer()
  194. Group {
  195. Text(state.units == .mgdL ? state.low.description : state.low.asMmolL.description)
  196. .foregroundColor(!displayPickerLowThreshold ? .primary : .accentColor)
  197. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  198. }
  199. }
  200. .onTapGesture {
  201. displayPickerLowThreshold.toggle()
  202. }
  203. }
  204. .padding(.top)
  205. if displayPickerLowThreshold {
  206. let setting = PickerSettingsProvider.shared.settings.low
  207. Picker(selection: $state.low, label: Text("")) {
  208. ForEach(
  209. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  210. id: \.self
  211. ) { value in
  212. let displayValue = state.units == .mgdL ? value : value.asMmolL
  213. Text("\(displayValue.description)").tag(value)
  214. }
  215. }
  216. .pickerStyle(WheelPickerStyle())
  217. .frame(maxWidth: .infinity)
  218. }
  219. VStack {
  220. HStack {
  221. Text("High Threshold")
  222. Spacer()
  223. Group {
  224. Text(state.units == .mgdL ? state.high.description : state.high.asMmolL.description)
  225. .foregroundColor(!displayPickerHighThreshold ? .primary : .accentColor)
  226. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  227. }
  228. }
  229. .onTapGesture {
  230. displayPickerHighThreshold.toggle()
  231. }
  232. }
  233. .padding(.top)
  234. if displayPickerHighThreshold {
  235. let setting = PickerSettingsProvider.shared.settings.high
  236. Picker(selection: $state.high, label: Text("")) {
  237. ForEach(
  238. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  239. id: \.self
  240. ) { value in
  241. let displayValue = state.units == .mgdL ? value : value.asMmolL
  242. Text("\(displayValue.description)").tag(value)
  243. }
  244. }
  245. .pickerStyle(WheelPickerStyle())
  246. .frame(maxWidth: .infinity)
  247. }
  248. HStack(alignment: .top) {
  249. Text("""
  250. Set low and high glucose values for the main screen glucose graph and statistics
  251. Low Default: 70
  252. High Default: 180
  253. """
  254. )
  255. .lineLimit(nil)
  256. .font(.footnote)
  257. .foregroundColor(.secondary)
  258. Spacer()
  259. Button(
  260. action: {
  261. hintLabel = "Low and High Thresholds"
  262. selectedVerboseHint =
  263. AnyView(Text("""
  264. Default values are based on internationally accepted Time in Range values of 70-180 mg/dL (5.5-10 mmol/L)
  265. Set the values used in the main screen glucose graph and to determine Time in Range for Statistics.
  266. Note: These values are not used to calculate insulin dosing.
  267. """))
  268. shouldDisplayHint.toggle()
  269. },
  270. label: {
  271. HStack {
  272. Image(systemName: "questionmark.circle")
  273. }
  274. }
  275. ).buttonStyle(BorderlessButtonStyle())
  276. }.padding(.top)
  277. }.padding(.bottom)
  278. }.listRowBackground(Color.chart)
  279. }
  280. Section {
  281. VStack {
  282. Picker(
  283. selection: $state.forecastDisplayType,
  284. label: Text("Forecast Display Type")
  285. ) {
  286. ForEach(ForecastDisplayType.allCases) { selection in
  287. Text(selection.displayName).tag(selection)
  288. }
  289. }.padding(.top)
  290. HStack(alignment: .top) {
  291. Text("""
  292. Choose between the OpenAPS colored "Lines" or the "Cone" of Uncertainty for the Forecast Lines
  293. Default: Cone
  294. """
  295. )
  296. .font(.footnote)
  297. .foregroundColor(.secondary)
  298. .lineLimit(nil)
  299. Spacer()
  300. Button(
  301. action: {
  302. hintLabel = "Forecast Display Type"
  303. selectedVerboseHint =
  304. AnyView(Text("""
  305. Default: Cone
  306. This setting allows you to choose between the following two options for the Forecast lines (previously: Prediction Lines).
  307. Lines: Uses the IOB, COB, UAM, and ZT forecast lines from OpenAPS
  308. Cone: 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.
  309. """))
  310. shouldDisplayHint.toggle()
  311. },
  312. label: {
  313. HStack {
  314. Image(systemName: "questionmark.circle")
  315. }
  316. }
  317. ).buttonStyle(BorderlessButtonStyle())
  318. }.padding(.top)
  319. }.padding(.bottom)
  320. }.listRowBackground(Color.chart)
  321. SettingInputSection(
  322. decimalValue: $state.hours,
  323. booleanValue: $booleanPlaceholder,
  324. shouldDisplayHint: $shouldDisplayHint,
  325. selectedVerboseHint: Binding(
  326. get: { selectedVerboseHint },
  327. set: {
  328. selectedVerboseHint = $0.map { AnyView($0) }
  329. hintLabel = "X-Axis Interval Step"
  330. }
  331. ),
  332. units: state.units,
  333. type: .decimal("hours"),
  334. label: "X-Axis Interval Step",
  335. miniHint: "Determines how many hours are shown in the main graph",
  336. verboseHint: Text("""
  337. Default: 6 hours
  338. This setting determines how many hours are shown in each view of the main graph.
  339. The default setting of 6 hours uses 2, 4, 6, 12, and 24 hour views.
  340. A setting of 4 would use 1.3, 2, 4, 8, and 16 hour views.
  341. A setting of 9 would use 3, 4.5, 9, 18, and 36 hour views.
  342. """)
  343. )
  344. Section {
  345. VStack {
  346. Picker(
  347. selection: $state.totalInsulinDisplayType,
  348. label: Text("Total Insulin Display Type")
  349. ) {
  350. ForEach(TotalInsulinDisplayType.allCases) { selection in
  351. Text(selection.displayName).tag(selection)
  352. }
  353. }.padding(.top)
  354. HStack(alignment: .top) {
  355. Text(
  356. "Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph"
  357. )
  358. .font(.footnote)
  359. .foregroundColor(.secondary)
  360. .lineLimit(nil)
  361. Spacer()
  362. Button(
  363. action: {
  364. hintLabel = "Total Insulin Display Type"
  365. selectedVerboseHint =
  366. AnyView(Text("""
  367. Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph.
  368. Total Daily Dose: Displays the last 24 hours of total insulin administered, both basal and bolus.
  369. Total Insulin in Scope: Displays the total insulin administered since midnight, both basal and bolus.
  370. """))
  371. shouldDisplayHint.toggle()
  372. },
  373. label: {
  374. HStack {
  375. Image(systemName: "questionmark.circle")
  376. }
  377. }
  378. ).buttonStyle(BorderlessButtonStyle())
  379. }.padding(.top)
  380. }.padding(.bottom)
  381. }.listRowBackground(Color.chart)
  382. // TODO: this needs to be a picker: mmol/L or %
  383. SettingInputSection(
  384. decimalValue: $decimalPlaceholder,
  385. booleanValue: $state.overrideHbA1cUnit,
  386. shouldDisplayHint: $shouldDisplayHint,
  387. selectedVerboseHint: Binding(
  388. get: { selectedVerboseHint },
  389. set: {
  390. selectedVerboseHint = $0.map { AnyView($0) }
  391. hintLabel = "Override HbA1c Unit"
  392. }
  393. ),
  394. units: state.units,
  395. type: .boolean,
  396. label: "Override HbA1c Unit",
  397. miniHint: "Display HbA1c in mmol/mol or %",
  398. verboseHint: Text("Choose between displaying the HbA1c value in the statistics view as a percentage (Target of <6.5%) or mmol/mol (Target of <48 mmol/mol)"),
  399. headerText: "Trio Statistics"
  400. )
  401. // TODO: this needs to be a picker: choose bar chart or progress bar
  402. SettingInputSection(
  403. decimalValue: $decimalPlaceholder,
  404. booleanValue: $state.oneDimensionalGraph,
  405. shouldDisplayHint: $shouldDisplayHint,
  406. selectedVerboseHint: Binding(
  407. get: { selectedVerboseHint },
  408. set: {
  409. selectedVerboseHint = $0.map { AnyView($0) }
  410. hintLabel = "Standing / Laying TIR Chart"
  411. }
  412. ),
  413. units: state.units,
  414. type: .boolean,
  415. label: "Standing / Laying TIR Chart",
  416. miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
  417. verboseHint: Text("Standing / Laying TIR Chart… bla bla bla")
  418. )
  419. SettingInputSection(
  420. decimalValue: $state.carbsRequiredThreshold,
  421. booleanValue: $state.showCarbsRequiredBadge,
  422. shouldDisplayHint: $shouldDisplayHint,
  423. selectedVerboseHint: Binding(
  424. get: { selectedVerboseHint },
  425. set: {
  426. selectedVerboseHint = $0.map { AnyView($0) }
  427. hintLabel = "Show Carbs Required Badge"
  428. }
  429. ),
  430. units: state.units,
  431. type: .conditionalDecimal("carbsRequiredThreshold"),
  432. label: "Show Carbs Required Badge",
  433. conditionalLabel: "Carbs Required Threshold",
  434. miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
  435. verboseHint: Text("Show Carbs Required Badge… bla bla bla"),
  436. headerText: "Carbs Required Badge"
  437. )
  438. }
  439. .sheet(isPresented: $shouldDisplayHint) {
  440. SettingInputHintView(
  441. hintDetent: $hintDetent,
  442. shouldDisplayHint: $shouldDisplayHint,
  443. hintLabel: hintLabel ?? "",
  444. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  445. sheetTitle: "Help"
  446. )
  447. }
  448. .scrollContentBackground(.hidden).background(color)
  449. .onAppear(perform: configureView)
  450. .navigationBarTitle("User Interface")
  451. .navigationBarTitleDisplayMode(.automatic)
  452. }
  453. }
  454. }