UserInterfaceSettingsRootView.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  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. Descriptions of each option found below."
  64. )
  65. VStack(alignment: .leading, spacing: 5) {
  66. Text("System Default:").bold()
  67. Text("Follows the phone's current color scheme setting at that time.")
  68. }
  69. VStack(alignment: .leading, spacing: 5) {
  70. Text("Light:").bold()
  71. Text("Always in Light mode")
  72. }
  73. VStack(alignment: .leading, spacing: 5) {
  74. Text("Dark:").bold()
  75. Text("Always in Dark mode")
  76. }
  77. }
  78. )
  79. shouldDisplayHint.toggle()
  80. },
  81. label: {
  82. HStack {
  83. Image(systemName: "questionmark.circle")
  84. }
  85. }
  86. ).buttonStyle(BorderlessButtonStyle())
  87. }.padding(.top)
  88. }.padding(.bottom)
  89. }
  90. ).listRowBackground(Color.chart)
  91. Section {
  92. VStack {
  93. Picker(
  94. selection: $state.glucoseColorScheme,
  95. label: Text("Glucose Color Scheme")
  96. ) {
  97. ForEach(GlucoseColorScheme.allCases) { selection in
  98. Text(selection.displayName).tag(selection)
  99. }
  100. }.padding(.top)
  101. HStack(alignment: .center) {
  102. Text(
  103. "Choose between Static or Dynamic coloring for glucose readings."
  104. )
  105. .font(.footnote)
  106. .foregroundColor(.secondary)
  107. .lineLimit(nil)
  108. Spacer()
  109. Button(
  110. action: {
  111. hintLabel = "Glucose Color Scheme"
  112. selectedVerboseHint =
  113. AnyView(
  114. VStack(alignment: .leading, spacing: 10) {
  115. Text(
  116. "Set the color scheme for glucose readings on the main glucose graph, live activities, and bolus calculator. Descriptions for each option found below."
  117. )
  118. VStack(alignment: .leading, spacing: 5) {
  119. Text("Static:").bold()
  120. Text("Red = Below-Range")
  121. Text("Green = In-Range")
  122. Text("Yellow = Above-Range")
  123. }
  124. VStack(alignment: .leading, spacing: 5) {
  125. Text("Dynamic:").bold()
  126. Text("Green = At Target")
  127. Text(
  128. "Gradient Red = As readings approach and exceed below target, they gradually become more red."
  129. )
  130. Text(
  131. "Gradient Purple = As readings approach and exceed above target, they become more purple."
  132. )
  133. }
  134. }
  135. )
  136. shouldDisplayHint.toggle()
  137. },
  138. label: {
  139. HStack {
  140. Image(systemName: "questionmark.circle")
  141. }
  142. }
  143. ).buttonStyle(BorderlessButtonStyle())
  144. }.padding(.top)
  145. }.padding(.bottom)
  146. }.listRowBackground(Color.chart)
  147. Section(
  148. header: Text("Home View Settings"),
  149. content: {
  150. VStack {
  151. Toggle("Show X-Axis Grid Lines", isOn: $state.xGridLines)
  152. Toggle("Show Y-Axis Grid Lines", isOn: $state.yGridLines)
  153. HStack(alignment: .center) {
  154. Text(
  155. "Display the grid lines behind the glucose graph."
  156. )
  157. .font(.footnote)
  158. .foregroundColor(.secondary)
  159. .lineLimit(nil)
  160. Spacer()
  161. Button(
  162. action: {
  163. hintLabel = "Show Main Chart X- and Y-Axis Grid Lines"
  164. selectedVerboseHint =
  165. AnyView(
  166. Text("Choose whether or not to display one or both X- and Y-Axis grid lines.")
  167. )
  168. shouldDisplayHint.toggle()
  169. },
  170. label: {
  171. HStack {
  172. Image(systemName: "questionmark.circle")
  173. }
  174. }
  175. ).buttonStyle(BorderlessButtonStyle())
  176. }.padding(.top)
  177. }.padding(.vertical)
  178. }
  179. ).listRowBackground(Color.chart)
  180. SettingInputSection(
  181. decimalValue: $decimalPlaceholder,
  182. booleanValue: $state.rulerMarks,
  183. shouldDisplayHint: $shouldDisplayHint,
  184. selectedVerboseHint: Binding(
  185. get: { selectedVerboseHint },
  186. set: {
  187. selectedVerboseHint = $0.map { AnyView($0) }
  188. hintLabel = "Show Low and High Thresholds"
  189. }
  190. ),
  191. units: state.units,
  192. type: .boolean,
  193. label: "Show Low and High Thresholds",
  194. miniHint: "Display the Low and High glucose thresholds set below.",
  195. verboseHint: VStack(alignment: .leading, spacing: 10) {
  196. Text("This setting displays the upper and lower values for your glucose target range.")
  197. Text("This range is for display and statistical purposes only and does not influence insulin dosing.")
  198. }
  199. )
  200. if state.rulerMarks {
  201. Section {
  202. VStack {
  203. VStack {
  204. HStack {
  205. Text("Low Threshold")
  206. Spacer()
  207. Group {
  208. Text(state.units == .mgdL ? state.low.description : state.low.asMmolL.description)
  209. .foregroundColor(!displayPickerLowThreshold ? .primary : .accentColor)
  210. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  211. }
  212. }
  213. .onTapGesture {
  214. displayPickerLowThreshold.toggle()
  215. }
  216. }
  217. .padding(.top)
  218. if displayPickerLowThreshold {
  219. let setting = PickerSettingsProvider.shared.settings.low
  220. Picker(selection: $state.low, label: Text("")) {
  221. ForEach(
  222. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  223. id: \.self
  224. ) { value in
  225. let displayValue = state.units == .mgdL ? value : value.asMmolL
  226. Text("\(displayValue.description)").tag(value)
  227. }
  228. }
  229. .pickerStyle(WheelPickerStyle())
  230. .frame(maxWidth: .infinity)
  231. }
  232. VStack {
  233. HStack {
  234. Text("High Threshold")
  235. Spacer()
  236. Group {
  237. Text(state.units == .mgdL ? state.high.description : state.high.asMmolL.description)
  238. .foregroundColor(!displayPickerHighThreshold ? .primary : .accentColor)
  239. Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
  240. }
  241. }
  242. .onTapGesture {
  243. displayPickerHighThreshold.toggle()
  244. }
  245. }
  246. .padding(.top)
  247. if displayPickerHighThreshold {
  248. let setting = PickerSettingsProvider.shared.settings.high
  249. Picker(selection: $state.high, label: Text("")) {
  250. ForEach(
  251. PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
  252. id: \.self
  253. ) { value in
  254. let displayValue = state.units == .mgdL ? value : value.asMmolL
  255. Text("\(displayValue.description)").tag(value)
  256. }
  257. }
  258. .pickerStyle(WheelPickerStyle())
  259. .frame(maxWidth: .infinity)
  260. }
  261. HStack(alignment: .center) {
  262. Text(
  263. "Set low and high glucose values for the main screen glucose graph and statistics."
  264. )
  265. .lineLimit(nil)
  266. .font(.footnote)
  267. .foregroundColor(.secondary)
  268. Spacer()
  269. Button(
  270. action: {
  271. hintLabel = "Low and High Thresholds"
  272. selectedVerboseHint =
  273. AnyView(
  274. VStack(alignment: .leading, spacing: 10) {
  275. Text(
  276. "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)."
  277. ).bold()
  278. Text(
  279. "Adjust these values if you would like the statistics to reflect different values than the internationally accepted Time In Range values used as the default."
  280. )
  281. Text("Note: These values are not used to calculate insulin dosing.")
  282. }
  283. )
  284. shouldDisplayHint.toggle()
  285. },
  286. label: {
  287. HStack {
  288. Image(systemName: "questionmark.circle")
  289. }
  290. }
  291. ).buttonStyle(BorderlessButtonStyle())
  292. }.padding(.top)
  293. }.padding(.bottom)
  294. }.listRowBackground(Color.chart)
  295. }
  296. Section {
  297. VStack {
  298. Picker(
  299. selection: $state.forecastDisplayType,
  300. label: Text("Forecast Display Type")
  301. ) {
  302. ForEach(ForecastDisplayType.allCases) { selection in
  303. Text(selection.displayName).tag(selection)
  304. }
  305. }.padding(.top)
  306. HStack(alignment: .center) {
  307. Text(
  308. "Choose between the Cone of Uncertainty or the OpenAPS colored lines for the algorithm's forecast."
  309. )
  310. .font(.footnote)
  311. .foregroundColor(.secondary)
  312. .lineLimit(nil)
  313. Spacer()
  314. Button(
  315. action: {
  316. hintLabel = "Forecast Display Type"
  317. selectedVerboseHint =
  318. AnyView(
  319. VStack(alignment: .leading, spacing: 10) {
  320. Text(
  321. "This setting allows you to choose between Cone of Uncertainty and OpenAPS Forecast Lines for the glucose forecast. Descriptions for each option found below."
  322. )
  323. VStack(alignment: .leading, spacing: 5) {
  324. Text("Cone:").bold()
  325. Text(
  326. "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."
  327. )
  328. }
  329. VStack(alignment: .leading, spacing: 5) {
  330. Text("Forecast Lines:").bold()
  331. Text(
  332. "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."
  333. )
  334. }
  335. }
  336. )
  337. shouldDisplayHint.toggle()
  338. },
  339. label: {
  340. HStack {
  341. Image(systemName: "questionmark.circle")
  342. }
  343. }
  344. ).buttonStyle(BorderlessButtonStyle())
  345. }.padding(.top)
  346. }.padding(.bottom)
  347. }.listRowBackground(Color.chart)
  348. Section {
  349. VStack(alignment: .leading) {
  350. Picker(
  351. selection: $state.totalInsulinDisplayType,
  352. label: Text("Total Insulin Display Type").multilineTextAlignment(.leading)
  353. ) {
  354. ForEach(TotalInsulinDisplayType.allCases) { selection in
  355. Text(selection.displayName).tag(selection)
  356. }
  357. }.padding(.top)
  358. HStack(alignment: .center) {
  359. Text(
  360. "Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph."
  361. )
  362. .font(.footnote)
  363. .foregroundColor(.secondary)
  364. .lineLimit(nil)
  365. Spacer()
  366. Button(
  367. action: {
  368. hintLabel = "Total Insulin Display Type"
  369. selectedVerboseHint =
  370. AnyView(
  371. VStack(alignment: .leading, spacing: 10) {
  372. Text(
  373. "Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph. Descriptions for each option found below."
  374. )
  375. VStack(alignment: .leading, spacing: 5) {
  376. Text("Total Daily Dose:").bold()
  377. Text(
  378. "Displays the last 24 hours of total insulin administered, both basal and bolus."
  379. )
  380. }
  381. VStack(alignment: .leading, spacing: 5) {
  382. Text("Total Insulin in Scope:").bold()
  383. Text(
  384. "Displays the total insulin administered since midnight, both basal and bolus."
  385. )
  386. }
  387. }
  388. )
  389. shouldDisplayHint.toggle()
  390. },
  391. label: {
  392. HStack {
  393. Image(systemName: "questionmark.circle")
  394. }
  395. }
  396. ).buttonStyle(BorderlessButtonStyle())
  397. }.padding(.top)
  398. }.padding(.bottom)
  399. }.listRowBackground(Color.chart)
  400. Section(
  401. header: Text("Trio Statistics"),
  402. content: {
  403. VStack {
  404. Picker(
  405. selection: $state.hbA1cDisplayUnit,
  406. label: Text("HbA1c Display Unit")
  407. ) {
  408. ForEach(HbA1cDisplayUnit.allCases) { selection in
  409. Text(selection.displayName).tag(selection)
  410. }
  411. }.padding(.top)
  412. HStack(alignment: .center) {
  413. Text(
  414. "Choose to display HbA1c in % or mmol/mol."
  415. )
  416. .font(.footnote)
  417. .foregroundColor(.secondary)
  418. .lineLimit(nil)
  419. Spacer()
  420. Button(
  421. action: {
  422. hintLabel = "HbA1c Display Unit"
  423. selectedVerboseHint =
  424. AnyView(
  425. Text(
  426. "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)."
  427. )
  428. )
  429. shouldDisplayHint.toggle()
  430. },
  431. label: {
  432. HStack {
  433. Image(systemName: "questionmark.circle")
  434. }
  435. }
  436. ).buttonStyle(BorderlessButtonStyle())
  437. }.padding(.top)
  438. }.padding(.bottom)
  439. }
  440. ).listRowBackground(Color.chart)
  441. Section {
  442. VStack(alignment: .leading) {
  443. Picker(
  444. selection: $state.timeInRangeChartStyle,
  445. label: Text("Time in Range Chart Style").multilineTextAlignment(.leading)
  446. ) {
  447. ForEach(TimeInRangeChartStyle.allCases) { selection in
  448. Text(selection.displayName).tag(selection)
  449. }
  450. }.padding(.top)
  451. HStack(alignment: .center) {
  452. Text(
  453. "Choose to display the Time in Range chart as a vertical bar chart or horizontal line chart."
  454. )
  455. .font(.footnote)
  456. .foregroundColor(.secondary)
  457. .lineLimit(nil)
  458. Spacer()
  459. Button(
  460. action: {
  461. hintLabel = "Time in Range Chart Style"
  462. selectedVerboseHint =
  463. AnyView(
  464. Text(
  465. "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."
  466. )
  467. )
  468. shouldDisplayHint.toggle()
  469. },
  470. label: {
  471. HStack {
  472. Image(systemName: "questionmark.circle")
  473. }
  474. }
  475. ).buttonStyle(BorderlessButtonStyle())
  476. }.padding(.top)
  477. }.padding(.bottom)
  478. }.listRowBackground(Color.chart)
  479. SettingInputSection(
  480. decimalValue: $state.carbsRequiredThreshold,
  481. booleanValue: $state.showCarbsRequiredBadge,
  482. shouldDisplayHint: $shouldDisplayHint,
  483. selectedVerboseHint: Binding(
  484. get: { selectedVerboseHint },
  485. set: {
  486. selectedVerboseHint = $0.map { AnyView($0) }
  487. hintLabel = "Show Carbs Required Badge"
  488. }
  489. ),
  490. units: state.units,
  491. type: .conditionalDecimal("carbsRequiredThreshold"),
  492. label: "Show Carbs Required Badge",
  493. conditionalLabel: "Carbs Required Threshold",
  494. miniHint: "Show carbs required as a notification badge on the home screen.",
  495. verboseHint: Text(
  496. "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.\n\nOnce enabled, set the Carbs Required Threshold to the lowest number of carbs you'd like to be recommended. A recommendation will not be given if carbs required is below this number."
  497. ),
  498. headerText: "Carbs Required Badge"
  499. )
  500. }
  501. .listSectionSpacing(sectionSpacing)
  502. .sheet(isPresented: $shouldDisplayHint) {
  503. SettingInputHintView(
  504. hintDetent: $hintDetent,
  505. shouldDisplayHint: $shouldDisplayHint,
  506. hintLabel: hintLabel ?? "",
  507. hintText: selectedVerboseHint ?? AnyView(EmptyView()),
  508. sheetTitle: "Help"
  509. )
  510. }
  511. .scrollContentBackground(.hidden)
  512. .background(appState.trioBackgroundColor(for: colorScheme))
  513. .onAppear(perform: configureView)
  514. .navigationBarTitle("User Interface")
  515. .navigationBarTitleDisplayMode(.automatic)
  516. }
  517. }
  518. }