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("Appearance")
  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 Trio's appearance. See hint for more details."
  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 Trio's apperance. 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 glucose reading color scheme. See hint for more details."
  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 glucose forecast presentation. See hint for more details."
  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 (Cone) and OpenAPS Forecast Lines (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 which total insulin calculation is displayed on the home screen. See hint for more details."
  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 amount of insulin given as a bolus (manual or SMB) and through temporary basal rates above zero during the selected timeframe of the main chart."
  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 percent 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 the orientation of the Time in Range 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 red icon on the main graph icon.",
  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.\n\nNote: The carbs suggested with this feature are to be used as a recommendation, not as a requirement. Depending on the current accuracy of your sensor and the accuracy of your settings, the suggested carbs can vary widely. Use your best judgement before injesting the suggested quanitity of carbs."
  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. }