| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- import Foundation
- import SwiftUI
- struct AddOverrideForm: View {
- @Environment(\.presentationMode) var presentationMode
- @Environment(\.colorScheme) var colorScheme
- @Environment(\.dismiss) var dismiss
- @Environment(AppState.self) var appState
- @Bindable var state: Adjustments.StateModel
- @State private var selectedIsfCrOption: IsfAndOrCrOptions = .isfAndCr
- @State private var selectedDisableSmbOption: DisableSmbOptions = .dontDisable
- @State private var percentageStep: Int = 5
- @State private var displayPickerPercentage: Bool = false
- @State private var displayPickerDuration: Bool = false
- @State private var targetStep: Decimal = 5
- @State private var displayPickerTarget: Bool = false
- @State private var displayPickerDisableSmbSchedule: Bool = false
- @State private var displayPickerSmbMinutes: Bool = false
- @State private var durationHours = 0
- @State private var durationMinutes = 0
- @State private var overrideTarget = false
- @State private var didPressSave = false
- var body: some View {
- NavigationView {
- List {
- addOverride()
- saveButton
- }
- .listSectionSpacing(10)
- .padding(.top, 30)
- .ignoresSafeArea(edges: .top)
- .scrollContentBackground(.hidden)
- .background(appState.trioBackgroundColor(for: colorScheme))
- .navigationTitle("Add Override")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .topBarLeading) {
- Button(action: {
- presentationMode.wrappedValue.dismiss()
- }, label: {
- Text("Cancel")
- })
- }
- ToolbarItem(placement: .topBarTrailing) {
- Button(
- action: {
- state.isHelpSheetPresented.toggle()
- },
- label: {
- Image(systemName: "questionmark.circle")
- }
- )
- }
- }
- .onAppear { targetStep = state.units == .mgdL ? 5 : 9 }
- .sheet(isPresented: $state.isHelpSheetPresented) {
- NavigationStack {
- List {
- Text(
- "This feature can be used to override these therepy settings for a chosen length of time:\n• Basal Rate\n ◦ Insulin Sensitivity\n ◦ Carb Ratio\n• Glucose Target"
- )
- Text(
- "You can also override your Max SMB Minutes and Max UAM SMB Minutes, as well as an option to disable SMBs."
- )
- Text(
- "Select \"Start Override\" to immediate start using the Override, or select \"Save as Preset\" to be able to easily start the Override at a later time."
- )
- Text(
- "If using Dynamic ISF (without Sigmoid), overriding your ISF will only adjust the limits of the ISF the algorithm is allowed to set.\n\nIf using Dynamic ISF (with Sigmoid), overriding your ISF will adjust the ISF used at your glucose target which extends to the ISF used at other glucose. Overriding your glucose target will change glucose level your ISF will be set to your profile ISF. Both of these can be combined in a single Override."
- )
- }
- .padding(.trailing, 10)
- .navigationBarTitle("Help", displayMode: .inline)
- Button { state.isHelpSheetPresented.toggle() }
- label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
- .buttonStyle(.bordered)
- .padding(.top)
- }
- .padding()
- .presentationDetents(
- [.fraction(0.9), .large],
- selection: $state.helpSheetDetent
- )
- }
- }
- }
- @ViewBuilder private func addOverride() -> some View {
- Group {
- Section {
- HStack {
- Text("Name")
- Spacer()
- TextField("(Optional)", text: $state.overrideName).multilineTextAlignment(.trailing)
- }
- }
- .listRowBackground(Color.chart)
- Section(footer: state.percentageDescription(state.overridePercentage)) {
- // Percentage Picker
- HStack {
- Text("Change Basal Rate by")
- Spacer()
- Text("\(state.overridePercentage.formatted(.number)) %")
- .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)
- .onTapGesture {
- displayPickerPercentage = toggleScrollWheel(displayPickerPercentage)
- }
- }
- if displayPickerPercentage {
- HStack {
- // Radio buttons and text on the left side
- VStack(alignment: .leading) {
- // Radio buttons for step iteration
- ForEach([1, 5], id: \.self) { step in
- RadioButton(isSelected: percentageStep == step, label: "\(step) %") {
- percentageStep = step
- state.overridePercentage = Adjustments.StateModel.roundOverridePercentageToStep(
- state.overridePercentage,
- step
- )
- }
- .padding(.top, 10)
- }
- }
- .frame(maxWidth: .infinity)
- Spacer()
- // Picker on the right side
- Picker(
- selection: Binding(
- get: { Int(truncating: state.overridePercentage as NSNumber) },
- set: { state.overridePercentage = Double($0) }
- ), label: Text("")
- ) {
- ForEach(Array(stride(from: 40, through: 150, by: percentageStep)), id: \.self) { percent in
- Text("\(percent) %").tag(percent)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- .frame(maxWidth: .infinity)
- .listRowSeparator(.hidden, edges: .top)
- }
- // Picker for ISF/CR settings
- Picker("Also Inversely Change", selection: $selectedIsfCrOption) {
- ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
- Text(option.rawValue).tag(option)
- }
- }
- .pickerStyle(MenuPickerStyle())
- .onChange(of: selectedIsfCrOption) { _, newValue in
- switch newValue {
- case .isfAndCr:
- state.isfAndCr = true
- state.isf = true
- state.cr = true
- case .isf:
- state.isfAndCr = false
- state.isf = true
- state.cr = false
- case .cr:
- state.isfAndCr = false
- state.isf = false
- state.cr = true
- case .nothing:
- state.isfAndCr = false
- state.isf = false
- state.cr = false
- }
- }
- }
- .listRowBackground(Color.chart)
- Section {
- Toggle(isOn: $state.shouldOverrideTarget) {
- Text("Override Target")
- }
- if state.shouldOverrideTarget {
- let settingsProvider = PickerSettingsProvider.shared
- let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 72, max: 270, type: .glucose)
- TargetPicker(
- label: "Target Glucose",
- selection: Binding(
- get: { state.target },
- set: { state.target = $0 }
- ),
- options: settingsProvider.generatePickerValues(
- from: glucoseSetting,
- units: state.units,
- roundMinToStep: true
- ),
- units: state.units,
- targetStep: $targetStep,
- displayPickerTarget: $displayPickerTarget,
- toggleScrollWheel: toggleScrollWheel
- )
- .onAppear {
- if state.target == 0 {
- state.target = 100
- }
- }
- }
- }
- .listRowBackground(Color.chart)
- Section {
- // Picker for ISF/CR settings
- Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
- ForEach(DisableSmbOptions.allCases, id: \.self) { option in
- Text(option.rawValue).tag(option)
- }
- }
- .pickerStyle(MenuPickerStyle())
- .onChange(of: selectedDisableSmbOption) { _, newValue in
- switch newValue {
- case .dontDisable:
- state.smbIsOff = false
- state.smbIsScheduledOff = false
- case .disable:
- state.smbIsOff = true
- state.smbIsScheduledOff = false
- case .disableOnSchedule:
- state.smbIsOff = false
- state.smbIsScheduledOff = true
- }
- }
- if state.smbIsScheduledOff {
- // First Hour SMBs Are Disabled
- HStack {
- Text("From")
- Spacer()
- Text(
- state.is24HourFormat() ? state.format24Hour(Int(truncating: state.start as NSNumber)) + ":00" :
- state.convertTo12HourFormat(Int(truncating: state.start as NSNumber))
- )
- .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
- .onTapGesture {
- displayPickerDisableSmbSchedule = toggleScrollWheel(displayPickerDisableSmbSchedule)
- }
- Spacer()
- Divider().frame(width: 1, height: 20)
- Spacer()
- Text("To")
- Spacer()
- Text(
- state.is24HourFormat() ? state.format24Hour(Int(truncating: state.end as NSNumber)) + ":00" :
- state.convertTo12HourFormat(Int(truncating: state.end as NSNumber))
- )
- .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
- .onTapGesture {
- displayPickerDisableSmbSchedule = toggleScrollWheel(displayPickerDisableSmbSchedule)
- }
- Spacer()
- }
- if displayPickerDisableSmbSchedule {
- HStack {
- // From Picker
- Picker(selection: Binding(
- get: { Int(truncating: state.start as NSNumber) },
- set: { state.start = Decimal($0) }
- ), label: Text("")) {
- ForEach(0 ..< 24, id: \.self) { hour in
- Text(
- state.is24HourFormat() ? state.format24Hour(hour) + ":00" : state
- .convertTo12HourFormat(hour)
- )
- .tag(hour)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- // To Picker
- Picker(selection: Binding(
- get: { Int(truncating: state.end as NSNumber) },
- set: { state.end = Decimal($0) }
- ), label: Text("")) {
- ForEach(0 ..< 24, id: \.self) { hour in
- Text(
- state.is24HourFormat() ? state.format24Hour(hour) + ":00" : state
- .convertTo12HourFormat(hour)
- )
- .tag(hour)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- .listRowBackground(Color.chart)
- if !state.smbIsOff {
- Section {
- Toggle(isOn: $state.advancedSettings) {
- Text("Override Max SMB Minutes")
- }
- if state.advancedSettings {
- // SMB Minutes Picker
- HStack {
- Text("SMB")
- Spacer()
- Text("\(state.smbMinutes.formatted(.number)) min")
- .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
- .onTapGesture {
- displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes)
- }
- Spacer()
- Divider().frame(width: 1, height: 20)
- Spacer()
- Text("UAM")
- Spacer()
- Text("\(state.uamMinutes.formatted(.number)) min")
- .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
- .onTapGesture {
- displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes)
- }
- }
- if displayPickerSmbMinutes {
- HStack {
- Picker(selection: Binding(
- get: { Int(truncating: state.smbMinutes as NSNumber) },
- set: { state.smbMinutes = Decimal($0) }
- ), label: Text("")) {
- ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
- Text("\(minute) min").tag(minute)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- Picker(selection: Binding(
- get: { Int(truncating: state.uamMinutes as NSNumber) },
- set: { state.uamMinutes = Decimal($0) }
- ), label: Text("")) {
- ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
- Text("\(minute) min").tag(minute)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- .listRowBackground(Color.chart)
- }
- Section {
- Toggle(isOn: $state.indefinite) {
- Text("Enable Indefinitely")
- }
- if !state.indefinite {
- HStack {
- Text("Duration")
- Spacer()
- Text(state.formatHrMin(Int(state.overrideDuration)))
- .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
- .onTapGesture {
- displayPickerDuration = toggleScrollWheel(displayPickerDuration)
- }
- }
- if displayPickerDuration {
- HStack {
- Picker("Hours", selection: $durationHours) {
- ForEach(0 ..< 24) { hour in
- Text("\(hour) hr").tag(hour)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- .onChange(of: durationHours) {
- state.overrideDuration = state.convertToMinutes(durationHours, durationMinutes)
- }
- Picker("Minutes", selection: $durationMinutes) {
- ForEach(Array(stride(from: 0, through: 55, by: 5)), id: \.self) { minute in
- Text("\(minute) min").tag(minute)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- .onChange(of: durationMinutes) {
- state.overrideDuration = state.convertToMinutes(durationHours, durationMinutes)
- }
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- .listRowBackground(Color.chart)
- }
- }
- private var saveButton: some View {
- let (isInvalid, errorMessage) = isOverrideInvalid()
- return Group {
- Section(
- header:
- HStack {
- Spacer()
- Text(errorMessage ?? "").textCase(nil)
- .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
- Spacer()
- },
- content: {
- Button(action: {
- Task {
- if state.indefinite { state.overrideDuration = 0 }
- state.isEnabled.toggle()
- await state.saveCustomOverride()
- await state.resetStateVariables()
- dismiss()
- }
- }, label: {
- Text("Start Override")
- })
- .disabled(isInvalid)
- .frame(maxWidth: .infinity, alignment: .center)
- .tint(.white)
- }
- ).listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
- Section {
- Button(action: {
- Task {
- await state.saveOverridePreset()
- dismiss()
- }
- }, label: {
- Text("Save as Preset")
- })
- .disabled(isInvalid)
- .frame(maxWidth: .infinity, alignment: .center)
- .tint(.white)
- }
- .listRowBackground(
- isInvalid ? Color(.systemGray4) : Color.secondary
- )
- }
- }
- private func toggleScrollWheel(_ toggle: Bool) -> Bool {
- displayPickerDuration = false
- displayPickerPercentage = false
- displayPickerTarget = false
- displayPickerDisableSmbSchedule = false
- displayPickerSmbMinutes = false
- return !toggle
- }
- private func isOverrideInvalid() -> (Bool, String?) {
- let noDurationSpecified = !state.indefinite && state.overrideDuration == 0
- let targetZeroWithOverride = state.shouldOverrideTarget && state.target == 0
- let allSettingsDefault = state.overridePercentage == 100 && !state.shouldOverrideTarget &&
- !state.advancedSettings && !state.smbIsOff && !state.smbIsScheduledOff
- if noDurationSpecified {
- return (true, "Enable indefinitely or set a duration.")
- }
- if targetZeroWithOverride {
- return (true, "Target glucose is out of range (\(state.units == .mgdL ? "72-270" : "4-14")).")
- }
- if allSettingsDefault {
- return (true, "All settings are at default values.")
- }
- return (false, nil)
- }
- }
|