UserInterfaceSettingsRootView.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  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. @Environment(AppState.self) var appState
  18. private var glucoseFormatter: NumberFormatter {
  19. let formatter = NumberFormatter()
  20. formatter.numberStyle = .decimal
  21. formatter.maximumFractionDigits = 0
  22. if state.units == .mmolL {
  23. formatter.maximumFractionDigits = 1
  24. }
  25. formatter.roundingMode = .halfUp
  26. return formatter
  27. }
  28. private var carbsFormatter: NumberFormatter {
  29. let formatter = NumberFormatter()
  30. formatter.numberStyle = .decimal
  31. formatter.maximumFractionDigits = 0
  32. return formatter
  33. }
  34. var body: some View {
  35. List {
  36. Section(
  37. header: Text("General Appearance"),
  38. content: {
  39. VStack {
  40. Picker(
  41. selection: $colorSchemePreference,
  42. label: Text("Trio Color Scheme")
  43. ) {
  44. ForEach(ColorSchemeOption.allCases) { selection in
  45. Text(selection.displayName).tag(selection)
  46. }
  47. }.padding(.top)
  48. HStack(alignment: .center) {
  49. Text(
  50. "Choose between Light, Dark, or System Default for the app color scheme."
  51. )
  52. .font(.footnote)
  53. .foregroundColor(.secondary)
  54. .lineLimit(nil)
  55. Spacer()
  56. Button(
  57. action: {
  58. hintLabel = "Color Scheme Preference"
  59. selectedVerboseHint =
  60. AnyView(
  61. VStack(alignment: .leading, spacing: 10) {
  62. Text(
  63. "Set the app color scheme using the following options:"
  64. )
  65. VStack(alignment: .leading, spacing: 10) {
  66. Text(
  67. "System Default: Follows the phone's current color scheme setting at that time"
  68. )
  69. Text("Light: Always in Light mode")
  70. Text("Dark: Always in Dark mode")
  71. }
  72. }
  73. )
  74. shouldDisplayHint.toggle()
  75. },
  76. label: {
  77. HStack {
  78. Image(systemName: "questionmark.circle")
  79. }
  80. }
  81. ).buttonStyle(BorderlessButtonStyle())
  82. }.padding(.top)
  83. }.padding(.bottom)
  84. }
  85. ).listRowBackground(Color.chart)
  86. Section {
  87. VStack {
  88. Picker(
  89. selection: $state.glucoseColorScheme,
  90. label: Text("Glucose Color Scheme")
  91. ) {
  92. ForEach(GlucoseColorScheme.allCases) { selection in
  93. Text(selection.displayName).tag(selection)
  94. }
  95. }.padding(.top)
  96. HStack(alignment: .center) {
  97. Text(
  98. "Choose between Static or Dynamic coloring for glucose readings."
  99. )
  100. .font(.footnote)
  101. .foregroundColor(.secondary)
  102. .lineLimit(nil)
  103. Spacer()
  104. Button(
  105. action: {
  106. hintLabel = "Glucose Color Scheme"
  107. selectedVerboseHint =
  108. AnyView(
  109. VStack(spacing: 10) {
  110. Text(
  111. "Set the color scheme for glucose readings on the main glucose graph, live activities, and bolus calculator using the following options:"
  112. )
  113. VStack {
  114. Text(
  115. "Static: Below-Range Target readings will be in RED, In-Range will be GREEN, Above-Range will be YELLOW."
  116. )
  117. Text(
  118. "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."
  119. )
  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: .center) {
  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(
  153. Text("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: VStack(alignment: .leading, spacing: 10) {
  183. Text("This setting displays the upper and lower values for your glucose target range.")
  184. Text("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: .center) {
  249. Text(
  250. "Set low and high glucose values for the main screen glucose graph and statistics."
  251. )
  252. .lineLimit(nil)
  253. .font(.footnote)
  254. .foregroundColor(.secondary)
  255. Spacer()
  256. Button(
  257. action: {
  258. hintLabel = "Low and High Thresholds"
  259. selectedVerboseHint =
  260. AnyView(
  261. VStack(alignment: .leading, spacing: 10) {
  262. Text(
  263. "Default values are based on internationally accepted Time in Range values of \(state.units == .mgdL ? "70" : 70.formattedAsMmolL)-\(state.units == .mgdL ? "180" : 180.formattedAsMmolL) \(state.units.rawValue)."
  264. )
  265. Text(
  266. "Set the values used in the main screen glucose graph and to determine Time in Range for Statistics."
  267. )
  268. Text("Note: These values are not used to calculate insulin dosing.")
  269. }
  270. )
  271. shouldDisplayHint.toggle()
  272. },
  273. label: {
  274. HStack {
  275. Image(systemName: "questionmark.circle")
  276. }
  277. }
  278. ).buttonStyle(BorderlessButtonStyle())
  279. }.padding(.top)
  280. }.padding(.bottom)
  281. }.listRowBackground(Color.chart)
  282. }
  283. Section {
  284. VStack {
  285. Picker(
  286. selection: $state.forecastDisplayType,
  287. label: Text("Forecast Display Type")
  288. ) {
  289. ForEach(ForecastDisplayType.allCases) { selection in
  290. Text(selection.displayName).tag(selection)
  291. }
  292. }.padding(.top)
  293. HStack(alignment: .center) {
  294. Text(
  295. "Choose between the Cone of Uncertainty or the OpenAPS colored lines for the algorithm's forecast."
  296. )
  297. .font(.footnote)
  298. .foregroundColor(.secondary)
  299. .lineLimit(nil)
  300. Spacer()
  301. Button(
  302. action: {
  303. hintLabel = "Forecast Display Type"
  304. selectedVerboseHint =
  305. AnyView(
  306. VStack(alignment: .leading, spacing: 10) {
  307. Text(
  308. "This setting allows you to choose between the following two options for the glucose forecast:"
  309. )
  310. Text(
  311. "Cone of Uncertainty: 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."
  312. )
  313. Text(
  314. "Forecast Lines: Uses the IOB, COB, UAM, and ZT forecast lines from OpenAPS. This option provides a more detailed view of the algorithm's forecast, but may be more confusing for some users."
  315. )
  316. }
  317. )
  318. shouldDisplayHint.toggle()
  319. },
  320. label: {
  321. HStack {
  322. Image(systemName: "questionmark.circle")
  323. }
  324. }
  325. ).buttonStyle(BorderlessButtonStyle())
  326. }.padding(.top)
  327. }.padding(.bottom)
  328. }.listRowBackground(Color.chart)
  329. Section {
  330. VStack(alignment: .leading) {
  331. Picker(
  332. selection: $state.totalInsulinDisplayType,
  333. label: Text("Total Insulin Display Type").multilineTextAlignment(.leading)
  334. ) {
  335. ForEach(TotalInsulinDisplayType.allCases) { selection in
  336. Text(selection.displayName).tag(selection)
  337. }
  338. }.padding(.top)
  339. HStack(alignment: .center) {
  340. Text(
  341. "Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph."
  342. )
  343. .font(.footnote)
  344. .foregroundColor(.secondary)
  345. .lineLimit(nil)
  346. Spacer()
  347. Button(
  348. action: {
  349. hintLabel = "Total Insulin Display Type"
  350. selectedVerboseHint =
  351. AnyView(
  352. VStack(alignment: .leading, spacing: 10) {
  353. Text(
  354. "Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph."
  355. )
  356. Text(
  357. "Total Daily Dose: Displays the last 24 hours of total insulin administered, both basal and bolus."
  358. )
  359. Text(
  360. "Total Insulin in Scope: Displays the total insulin administered since midnight, both basal and bolus."
  361. )
  362. }
  363. )
  364. shouldDisplayHint.toggle()
  365. },
  366. label: {
  367. HStack {
  368. Image(systemName: "questionmark.circle")
  369. }
  370. }
  371. ).buttonStyle(BorderlessButtonStyle())
  372. }.padding(.top)
  373. }.padding(.bottom)
  374. }.listRowBackground(Color.chart)
  375. Section(
  376. header: Text("Trio Statistics"),
  377. content: {
  378. VStack {
  379. Picker(
  380. selection: $state.hbA1cDisplayUnit,
  381. label: Text("HbA1c Display Unit")
  382. ) {
  383. ForEach(HbA1cDisplayUnit.allCases) { selection in
  384. Text(selection.displayName).tag(selection)
  385. }
  386. }.padding(.top)
  387. HStack(alignment: .center) {
  388. Text(
  389. "Choose to display HbA1c in % or mmol/mol."
  390. )
  391. .font(.footnote)
  392. .foregroundColor(.secondary)
  393. .lineLimit(nil)
  394. Spacer()
  395. Button(
  396. action: {
  397. hintLabel = "HbA1c Display Unit"
  398. selectedVerboseHint =
  399. AnyView(
  400. Text(
  401. "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)."
  402. )
  403. )
  404. shouldDisplayHint.toggle()
  405. },
  406. label: {
  407. HStack {
  408. Image(systemName: "questionmark.circle")
  409. }
  410. }
  411. ).buttonStyle(BorderlessButtonStyle())
  412. }.padding(.top)
  413. }.padding(.bottom)
  414. }
  415. ).listRowBackground(Color.chart)
  416. Section {
  417. VStack(alignment: .leading) {
  418. Picker(
  419. selection: $state.timeInRangeChartStyle,
  420. label: Text("Time in Range Chart Style").multilineTextAlignment(.leading)
  421. ) {
  422. ForEach(TimeInRangeChartStyle.allCases) { selection in
  423. Text(selection.displayName).tag(selection)
  424. }
  425. }.padding(.top)
  426. HStack(alignment: .center) {
  427. Text(
  428. "Choose to display the Time in Range chart as a vertical bar chart or horizontal line chart."
  429. )
  430. .font(.footnote)
  431. .foregroundColor(.secondary)
  432. .lineLimit(nil)
  433. Spacer()
  434. Button(
  435. action: {
  436. hintLabel = "Time in Range Chart Style"
  437. selectedVerboseHint =
  438. AnyView(
  439. Text(
  440. "Choose which style for the time in range chart you'd prefer: a standing, i.e., vertical, bar chart or a laying, i.e., horizontal, line chart."
  441. )
  442. )
  443. shouldDisplayHint.toggle()
  444. },
  445. label: {
  446. HStack {
  447. Image(systemName: "questionmark.circle")
  448. }
  449. }
  450. ).buttonStyle(BorderlessButtonStyle())
  451. }.padding(.top)
  452. }.padding(.bottom)
  453. }.listRowBackground(Color.chart)
  454. SettingInputSection(
  455. decimalValue: $state.carbsRequiredThreshold,
  456. booleanValue: $state.showCarbsRequiredBadge,
  457. shouldDisplayHint: $shouldDisplayHint,
  458. selectedVerboseHint: Binding(
  459. get: { selectedVerboseHint },
  460. set: {
  461. selectedVerboseHint = $0.map { AnyView($0) }
  462. hintLabel = "Show Carbs Required Badge"
  463. }
  464. ),
  465. units: state.units,
  466. type: .conditionalDecimal("carbsRequiredThreshold"),
  467. label: "Show Carbs Required Badge",
  468. conditionalLabel: "Carbs Required Threshold",
  469. miniHint: "Show carbs required as a notification badge on the home screen.",
  470. verboseHint: Text(
  471. "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."
  472. ),
  473. headerText: "Carbs Required Badge"
  474. )
  475. }
  476. .listSectionSpacing(sectionSpacing)
  477. .sheet(isPresented: $shouldDisplayHint) {
  478. SettingInputHintView(
  479. hintDetent: $hintDetent,
  480. shouldDisplayHint: $shouldDisplayHint,
  481. hintLabel: hintLabel ?? "",
  482. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  483. sheetTitle: "Help"
  484. )
  485. }
  486. .scrollContentBackground(.hidden)
  487. .background(appState.trioBackgroundColor(for: colorScheme))
  488. .onAppear(perform: configureView)
  489. .navigationBarTitle("User Interface")
  490. .navigationBarTitleDisplayMode(.automatic)
  491. }
  492. }
  493. }