UserInterfaceSettingsRootView.swift 26 KB

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