| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606 |
- import SwiftUI
- import Swinject
- extension UserInterfaceSettings {
- struct RootView: BaseView {
- let resolver: Resolver
- @StateObject var state = StateModel()
- @State private var shouldDisplayHint: Bool = false
- @State var hintDetent = PresentationDetent.large
- @State var selectedVerboseHint: AnyView?
- @State var hintLabel: String?
- @State private var decimalPlaceholder: Decimal = 0.0
- @State private var booleanPlaceholder: Bool = false
- @State private var displayPickerLowThreshold: Bool = false
- @State private var displayPickerHighThreshold: Bool = false
- @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
- @Environment(\.colorScheme) var colorScheme
- @Environment(AppState.self) var appState
- private var glucoseFormatter: NumberFormatter {
- let formatter = NumberFormatter()
- formatter.numberStyle = .decimal
- formatter.maximumFractionDigits = 0
- if state.units == .mmolL {
- formatter.maximumFractionDigits = 1
- }
- formatter.roundingMode = .halfUp
- return formatter
- }
- private var carbsFormatter: NumberFormatter {
- let formatter = NumberFormatter()
- formatter.numberStyle = .decimal
- formatter.maximumFractionDigits = 0
- return formatter
- }
- var body: some View {
- List {
- Section(
- header: Text("General Appearance"),
- content: {
- VStack {
- Picker(
- selection: $colorSchemePreference,
- label: Text("Appearance")
- ) {
- ForEach(ColorSchemeOption.allCases) { selection in
- Text(selection.displayName).tag(selection)
- }
- }.padding(.top)
- HStack(alignment: .center) {
- Text(
- "Choose Trio's appearance. See hint for more details."
- )
- .font(.footnote)
- .foregroundColor(.secondary)
- .lineLimit(nil)
- Spacer()
- Button(
- action: {
- hintLabel = String(localized: "Color Scheme Preference")
- selectedVerboseHint =
- AnyView(
- VStack(alignment: .leading, spacing: 10) {
- Text(
- "Sets Trio's appearance. Descriptions of each option found below."
- )
- VStack(alignment: .leading, spacing: 5) {
- Text("System Default:").bold()
- Text("Follows the phone's current color scheme setting at that time")
- }
- VStack(alignment: .leading, spacing: 5) {
- Text("Light:").bold()
- Text("Always in Light mode")
- }
- VStack(alignment: .leading, spacing: 5) {
- Text("Dark:").bold()
- Text("Always in Dark mode")
- }
- }
- )
- shouldDisplayHint.toggle()
- },
- label: {
- HStack {
- Image(systemName: "questionmark.circle")
- }
- }
- ).buttonStyle(BorderlessButtonStyle())
- }.padding(.top)
- }.padding(.bottom)
- }
- ).listRowBackground(Color.chart)
- Section {
- VStack {
- Picker(
- selection: $state.glucoseColorScheme,
- label: Text("Glucose Color Scheme")
- ) {
- ForEach(GlucoseColorScheme.allCases) { selection in
- Text(selection.displayName).tag(selection)
- }
- }.padding(.top)
- HStack(alignment: .center) {
- Text(
- "Choose glucose reading color scheme. See hint for more details."
- )
- .font(.footnote)
- .foregroundColor(.secondary)
- .lineLimit(nil)
- Spacer()
- Button(
- action: {
- hintLabel = String(localized: "Glucose Color Scheme")
- selectedVerboseHint =
- AnyView(
- VStack(alignment: .leading, spacing: 10) {
- Text(
- "Set the color scheme for glucose readings on the main glucose graph, live activities, and bolus calculator. Descriptions for each option found below."
- )
- VStack(alignment: .leading, spacing: 5) {
- Text("Static:").bold()
- Text("Red = Below Range")
- Text("Green = In Range")
- Text("Yellow = Above Range")
- }
- VStack(alignment: .leading, spacing: 5) {
- Text("Dynamic:").bold()
- Text("Green = At Target")
- Text(
- "Gradient Red = As readings approach and exceed below target, they gradually become more red."
- )
- Text(
- "Gradient Purple = As readings approach and exceed above target, they become more purple."
- )
- }
- }
- )
- shouldDisplayHint.toggle()
- },
- label: {
- HStack {
- Image(systemName: "questionmark.circle")
- }
- }
- ).buttonStyle(BorderlessButtonStyle())
- }.padding(.top)
- }.padding(.bottom)
- }.listRowBackground(Color.chart)
- Section(
- header: Text("Home View Settings"),
- content: {
- VStack {
- Toggle("Show X-Axis Grid Lines", isOn: $state.xGridLines)
- Toggle("Show Y-Axis Grid Lines", isOn: $state.yGridLines)
- HStack(alignment: .center) {
- Text(
- "Display the grid lines behind the glucose graph."
- )
- .font(.footnote)
- .foregroundColor(.secondary)
- .lineLimit(nil)
- Spacer()
- Button(
- action: {
- hintLabel = String(localized: "Show Main Chart X- and Y-Axis Grid Lines")
- selectedVerboseHint =
- AnyView(
- Text("Choose whether or not to display one or both X- and Y-Axis grid lines.")
- )
- shouldDisplayHint.toggle()
- },
- label: {
- HStack {
- Image(systemName: "questionmark.circle")
- }
- }
- ).buttonStyle(BorderlessButtonStyle())
- }.padding(.top)
- }.padding(.vertical)
- }
- ).listRowBackground(Color.chart)
- SettingInputSection(
- decimalValue: $decimalPlaceholder,
- booleanValue: $state.rulerMarks,
- shouldDisplayHint: $shouldDisplayHint,
- selectedVerboseHint: Binding(
- get: { selectedVerboseHint },
- set: {
- selectedVerboseHint = $0.map { AnyView($0) }
- hintLabel = String(localized: "Show Low and High Thresholds")
- }
- ),
- units: state.units,
- type: .boolean,
- label: String(localized: "Show Low and High Thresholds"),
- miniHint: String(localized: "Display the Low and High glucose thresholds set below."),
- verboseHint: VStack(alignment: .leading, spacing: 10) {
- Text("This setting displays the upper and lower values for your glucose target range.")
- Text("This range is for display and statistical purposes only and does not influence insulin dosing.")
- }
- )
- if state.rulerMarks {
- Section {
- VStack {
- VStack {
- HStack {
- Text("Low Threshold")
- Spacer()
- Group {
- Text(state.units == .mgdL ? state.low.description : state.low.asMmolL.description)
- .foregroundColor(!displayPickerLowThreshold ? .primary : .accentColor)
- Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
- }
- }
- .onTapGesture {
- displayPickerLowThreshold.toggle()
- }
- }
- .padding(.top)
- if displayPickerLowThreshold {
- let setting = PickerSettingsProvider.shared.settings.low
- Picker(selection: $state.low, label: Text("")) {
- ForEach(
- PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
- id: \.self
- ) { value in
- let displayValue = state.units == .mgdL ? value : value.asMmolL
- Text("\(displayValue.description)").tag(value)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- VStack {
- HStack {
- Text("High Threshold")
- Spacer()
- Group {
- Text(state.units == .mgdL ? state.high.description : state.high.asMmolL.description)
- .foregroundColor(!displayPickerHighThreshold ? .primary : .accentColor)
- Text(state.units == .mgdL ? " mg/dL" : " mmol/L").foregroundColor(.secondary)
- }
- }
- .onTapGesture {
- displayPickerHighThreshold.toggle()
- }
- }
- .padding(.top)
- if displayPickerHighThreshold {
- let setting = PickerSettingsProvider.shared.settings.high
- Picker(selection: $state.high, label: Text("")) {
- ForEach(
- PickerSettingsProvider.shared.generatePickerValues(from: setting, units: state.units),
- id: \.self
- ) { value in
- let displayValue = state.units == .mgdL ? value : value.asMmolL
- Text("\(displayValue.description)").tag(value)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- HStack(alignment: .center) {
- Text(
- "Set low and high glucose values for the main screen, watch app and live activity glucose graph."
- )
- .lineLimit(nil)
- .font(.footnote)
- .foregroundColor(.secondary)
- Spacer()
- Button(
- action: {
- hintLabel = String(localized: "Low and High Thresholds")
- selectedVerboseHint =
- AnyView(
- VStack(alignment: .leading, spacing: 10) {
- Text(
- "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)."
- ).bold()
- Text(
- "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."
- )
- Text("Note: These values are not used to calculate insulin dosing.")
- }
- )
- shouldDisplayHint.toggle()
- },
- label: {
- HStack {
- Image(systemName: "questionmark.circle")
- }
- }
- ).buttonStyle(BorderlessButtonStyle())
- }.padding(.top)
- }.padding(.bottom)
- }.listRowBackground(Color.chart)
- }
- Section {
- VStack {
- Picker(
- selection: $state.forecastDisplayType,
- label: Text("Forecast Display Type")
- ) {
- ForEach(ForecastDisplayType.allCases) { selection in
- Text(selection.displayName).tag(selection)
- }
- }.padding(.top)
- HStack(alignment: .center) {
- Text(
- "Choose glucose forecast presentation. See hint for more details."
- )
- .font(.footnote)
- .foregroundColor(.secondary)
- .lineLimit(nil)
- Spacer()
- Button(
- action: {
- hintLabel = String(localized: "Forecast Display Type")
- selectedVerboseHint =
- AnyView(
- VStack(alignment: .leading, spacing: 10) {
- Text(
- "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."
- )
- VStack(alignment: .leading, spacing: 5) {
- Text("Cone:").bold()
- Text(
- "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."
- )
- }
- VStack(alignment: .leading, spacing: 5) {
- Text("Forecast Lines:").bold()
- Text(
- "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."
- )
- }
- }
- )
- shouldDisplayHint.toggle()
- },
- label: {
- HStack {
- Image(systemName: "questionmark.circle")
- }
- }
- ).buttonStyle(BorderlessButtonStyle())
- }.padding(.top)
- }.padding(.bottom)
- }.listRowBackground(Color.chart)
- Section {
- VStack {
- Picker(
- selection: $state.bolusDisplayThreshold,
- label: Text("Bolus Display Threshold")
- ) {
- ForEach(BolusDisplayThreshold.allCases) { selection in
- Text(selection.displayName).tag(selection)
- }
- }.padding(.top)
- HStack(alignment: .center) {
- Text(
- "Choose to hide small bolus amounts. See hint for more details."
- )
- .font(.footnote)
- .foregroundColor(.secondary)
- .lineLimit(nil)
- Spacer()
- Button(
- action: {
- hintLabel = String(localized: "Bolus Display Threshold")
- selectedVerboseHint =
- AnyView(
- VStack(alignment: .leading) {
- Text(
- "This setting controls which bolus amount labels are shown on Trio’s main chart. Boluses appear as blue upside-down triangles, with a number showing the amount. Depending on the option you choose, only boluses at or above that amount will show a label. For example, if you choose ‘0.5 U and over’, only boluses of 0.5 U or more will show a label."
- )
- }
- )
- shouldDisplayHint.toggle()
- },
- label: {
- HStack {
- Image(systemName: "questionmark.circle")
- }
- }
- ).buttonStyle(BorderlessButtonStyle())
- }.padding(.top)
- }.padding(.bottom)
- }.listRowBackground(Color.chart)
- Section(
- header: Text("Trio Statistics"),
- content: {
- VStack {
- Picker(
- selection: $state.eA1cDisplayUnit,
- label: Text("eA1c/GMI Display Unit")
- ) {
- ForEach(EstimatedA1cDisplayUnit.allCases) { selection in
- Text(selection.displayName).tag(selection)
- }
- }.padding(.top)
- HStack(alignment: .center) {
- Text(
- "Choose to display eA1c and GMI in percent or mmol/mol."
- )
- .font(.footnote)
- .foregroundColor(.secondary)
- .lineLimit(nil)
- Spacer()
- Button(
- action: {
- hintLabel = String(localized: "eA1c/GMI Display Unit")
- selectedVerboseHint =
- AnyView(
- Text(
- "Choose which format you'd prefer the eA1c (estimated A1c) and GMI (Glucose Management Index) value in the statistics view as a percentage (Example: eA1c: 6.5%) or mmol/mol (Example: eA1c: 48 mmol/mol)."
- )
- )
- shouldDisplayHint.toggle()
- },
- label: {
- HStack {
- Image(systemName: "questionmark.circle")
- }
- }
- ).buttonStyle(BorderlessButtonStyle())
- }.padding(.top)
- }.padding(.bottom)
- }
- ).listRowBackground(Color.chart)
- Section {
- VStack(alignment: .leading) {
- Picker(
- selection: $state.timeInRangeType,
- label: Text("Time in Range Type").multilineTextAlignment(.leading)
- ) {
- ForEach(TimeInRangeType.allCases) { selection in
- Text(selection.displayName).tag(selection)
- }
- }.padding(.top)
- HStack(alignment: .center) {
- Text(
- "Choose type of time in range to be used for Trio's statistics."
- )
- .font(.footnote)
- .foregroundColor(.secondary)
- .lineLimit(nil)
- Spacer()
- Button(
- action: {
- hintLabel = String(localized: "Time in Range Type")
- selectedVerboseHint =
- AnyView(
- VStack(
- alignment: .leading,
- spacing: 10
- ) {
- Text(
- "Choose which type of time in range Trio should adopt for all its statistical charts and displays:"
- )
- VStack(
- alignment: .leading,
- spacing: 5
- ) {
- Text(
- "Time in Tight Range (TITR):"
- )
- .bold()
- let titrBottomThreshold =
- "\(state.units == .mgdL ? Decimal(70) : 70.asMmolL)"
- let titrTopThreshold =
- "\(state.units == .mgdL ? Decimal(140) : 140.asMmolL)"
- Text(String(
- localized: "Uses the fairly established Time in Tight Range definition, which is defined as time between \(titrBottomThreshold) and \(titrTopThreshold) \(state.units.rawValue).",
- comment: "Time in Tight Range (TITR) verbose hint description"
- ))
- }
- VStack(
- alignment: .leading,
- spacing: 5
- ) {
- Text(
- "Time in Normoglycemia (TING):"
- )
- .bold()
- let tingBottomThreshold =
- "\(state.units == .mgdL ? Decimal(63) : 63.asMmolL)"
- let tingTopThreshold =
- "\(state.units == .mgdL ? Decimal(140) : 140.asMmolL)"
- Text(String(
- localized: "Uses the very new – first discussed at ATTD 2025 in Amsterdam, NL – Time in Normoglycemia definition, which adopts its range as all values between the normoglycemic minimum threshold (\(tingBottomThreshold) \(state.units.rawValue)) and \(tingTopThreshold) \(state.units.rawValue).",
- comment: "Time in Normoglycemia (TING) verbose hint description"
- ))
- }
- }
- )
- shouldDisplayHint.toggle()
- },
- label: {
- HStack {
- Image(systemName: "questionmark.circle")
- }
- }
- ).buttonStyle(BorderlessButtonStyle())
- }.padding(.top)
- }.padding(.bottom)
- }.listRowBackground(Color.chart)
- SettingInputSection(
- decimalValue: $state.carbsRequiredThreshold,
- booleanValue: $state.showCarbsRequiredBadge,
- shouldDisplayHint: $shouldDisplayHint,
- selectedVerboseHint: Binding(
- get: { selectedVerboseHint },
- set: {
- selectedVerboseHint = $0.map { AnyView($0) }
- hintLabel = String(localized: "Show Carbs Required Badge")
- }
- ),
- units: state.units,
- type: .conditionalDecimal("carbsRequiredThreshold"),
- label: String(localized: "Show Carbs Required Badge"),
- conditionalLabel: String(localized: "Carbs Required Threshold"),
- miniHint: String(localized: "Show carbs required as a red icon on the main graph icon."),
- verboseHint: Text(
- "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."
- ),
- headerText: String(localized: "Carbs Required Badge")
- )
- SettingInputSection(
- decimalValue: $decimalPlaceholder,
- booleanValue: $state.requireAdjustmentsConfirmation,
- shouldDisplayHint: $shouldDisplayHint,
- selectedVerboseHint: Binding(
- get: { selectedVerboseHint },
- set: {
- selectedVerboseHint = $0.map { AnyView($0) }
- hintLabel = String(localized: "Require Adjustments Confirmation")
- }
- ),
- units: state.units,
- type: .boolean,
- label: String(localized: "Require Adjustments Confirmation"),
- miniHint: String(
- localized: "If enabled, a confirmation dialog will be shown when activating adjustment presets."
- ),
- verboseHint: Text(
- "Turning this on will show a confirmation dialog when you activate an Override or Temporary Target preset. This is for users who would like avoid accidentally activating a preset by mistake."
- ),
- headerText: String(localized: "Adjustments")
- )
- }
- .listSectionSpacing(sectionSpacing)
- .sheet(isPresented: $shouldDisplayHint) {
- SettingInputHintView(
- hintDetent: $hintDetent,
- shouldDisplayHint: $shouldDisplayHint,
- hintLabel: hintLabel ?? "",
- hintText: selectedVerboseHint ?? AnyView(EmptyView()),
- sheetTitle: String(localized: "Help", comment: "Help sheet title")
- )
- }
- .scrollContentBackground(.hidden)
- .background(appState.trioBackgroundColor(for: colorScheme))
- .onAppear(perform: configureView)
- .navigationBarTitle("User Interface")
- .navigationBarTitleDisplayMode(.automatic)
- }
- }
- }
|