| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666 |
- import Foundation
- import SwiftUI
- struct EditOverrideForm: View {
- var override: OverrideStored
- @Environment(\.presentationMode) var presentationMode
- @Environment(\.colorScheme) var colorScheme
- @Environment(AppState.self) var appState
- @Bindable var state: Adjustments.StateModel
- @State private var name: String
- @State private var percentage: Double
- @State private var indefinite: Bool
- @State private var duration: Decimal
- @State private var target: Decimal?
- @State private var advancedSettings: Bool
- @State private var smbIsOff: Bool
- @State private var smbIsScheduledOff: Bool
- @State private var start: Decimal?
- @State private var end: Decimal?
- @State private var isfAndCr: Bool
- @State private var isf: Bool
- @State private var cr: Bool
- @State private var smbMinutes: Decimal?
- @State private var uamMinutes: Decimal?
- @State private var selectedIsfCrOption: IsfAndOrCrOptions
- @State private var selectedDisableSmbOption: DisableSmbOptions
- @State private var hasChanges = false
- @State private var isEditing = false
- @State private var target_override = false
- @State private var percentageStep: Int = 1
- @State private var displayPickerPercentage: Bool = false
- @State private var displayPickerDuration: Bool = false
- @State private var targetStep: Decimal = 1
- @State private var displayPickerTarget: Bool = false
- @State private var displayPickerDisableSmbSchedule: Bool = false
- @State private var displayPickerSmbMinutes: Bool = false
- init(overrideToEdit: OverrideStored, state: Adjustments.StateModel) {
- override = overrideToEdit
- _state = Bindable(wrappedValue: state)
- _name = State(initialValue: overrideToEdit.name ?? "")
- _percentage = State(initialValue: overrideToEdit.percentage)
- _indefinite = State(initialValue: overrideToEdit.indefinite)
- _duration = State(initialValue: overrideToEdit.duration?.decimalValue ?? 0)
- _target = State(initialValue: overrideToEdit.target?.decimalValue)
- _target_override = State(initialValue: overrideToEdit.target != nil && overrideToEdit.target?.decimalValue != 0)
- _advancedSettings = State(initialValue: overrideToEdit.advancedSettings)
- _smbIsOff = State(initialValue: overrideToEdit.smbIsOff)
- _smbIsScheduledOff = State(initialValue: overrideToEdit.smbIsScheduledOff)
- _start = State(initialValue: overrideToEdit.start?.decimalValue)
- _end = State(initialValue: overrideToEdit.end?.decimalValue)
- _isfAndCr = State(initialValue: overrideToEdit.isfAndCr)
- _isf = State(initialValue: overrideToEdit.isf)
- _cr = State(initialValue: overrideToEdit.cr)
- _selectedIsfCrOption = State(
- initialValue: overrideToEdit.isfAndCr ? .isfAndCr
- : (overrideToEdit.isf ? .isf : (overrideToEdit.cr ? .cr : .nothing))
- )
- _selectedDisableSmbOption = State(
- initialValue: overrideToEdit.smbIsScheduledOff ? .disableOnSchedule
- : (overrideToEdit.smbIsOff ? .disable : .dontDisable)
- )
- _smbMinutes = State(initialValue: overrideToEdit.smbMinutes?.decimalValue)
- _uamMinutes = State(initialValue: overrideToEdit.uamMinutes?.decimalValue)
- }
- private var percentageSelection: Binding<Double> {
- Binding<Double>(
- get: {
- let value = floor(percentage / Double(percentageStep)) * Double(percentageStep)
- return max(10, min(value, 200))
- },
- set: {
- percentage = $0
- hasChanges = true
- }
- )
- }
- var body: some View {
- NavigationView {
- List {
- editOverride()
- saveButton
- }
- .listSectionSpacing(10)
- .padding(.top, 30)
- .ignoresSafeArea(edges: .top)
- .scrollContentBackground(.hidden)
- .background(appState.trioBackgroundColor(for: colorScheme))
- .navigationTitle("Edit 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")
- }
- )
- }
- }
- .onDisappear {
- if !hasChanges {
- // Reset UI changes
- resetValues()
- }
- }
- .sheet(isPresented: $state.isHelpSheetPresented) {
- NavigationStack {
- List {
- VStack(alignment: .leading, spacing: 10) {
- VStack(alignment: .leading, spacing: 0) {
- Text("This feature can be used to override these therapy settings for a chosen length of time:")
- .fixedSize(horizontal: false, vertical: true)
- Text("• Basal Rate")
- Text(" ◦ Insulin Sensitivity")
- Text(" ◦ Carb Ratio")
- Text("• Glucose Target")
- }
- Text(
- "There are also options to override your Max SMB Minutes and Max UAM SMB Minutes, as well as to disable SMBs."
- )
- Text(
- "Select \"Start Override\" to immediately start using the Override, or select \"Save as Preset\" to be able to easily start the Override at a later time."
- )
- Text(
- "If an active override preset is edited, the changes will also apply to the currently running override. However, if you edit the currently running override directly, the preset stays unchanged."
- )
- Text(
- "If using Dynamic ISF (without Sigmoid), overriding your ISF will only adjust the limits of the ISF the algorithm is allowed to set."
- )
- Text(
- "If 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 editOverride() -> some View {
- Group {
- if override.name != nil {
- Section {
- HStack {
- Text("Name")
- Spacer()
- TextField("Name", text: $name)
- .onChange(of: name) { hasChanges = true }
- .multilineTextAlignment(.trailing)
- }
- }
- .listRowBackground(Color.chart)
- }
- // Percentage Picker
- Section(footer: state.percentageDescription(percentage)) {
- HStack {
- Text("Change Basal Rate by")
- Spacer()
- Text("\(percentage.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
- percentage = Adjustments.StateModel.roundOverridePercentageToStep(percentage, step)
- }
- .padding(.top, 10)
- }
- }
- .frame(maxWidth: .infinity)
- Spacer()
- // Picker on the right side
- Picker(
- selection: percentageSelection,
- label: Text("")
- ) {
- ForEach(
- Array(stride(from: 40.0, through: 150.0, by: Double(percentageStep))),
- id: \.self
- ) { percent in
- Text("\(Int(percent)) %").tag(percent)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- // Picker for ISF/CR settings
- Picker("Also 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:
- isfAndCr = true
- isf = false
- cr = false
- case .isf:
- isfAndCr = false
- isf = true
- cr = false
- case .cr:
- isfAndCr = false
- isf = false
- cr = true
- case .nothing:
- isfAndCr = false
- isf = false
- cr = false
- }
- hasChanges = true
- }
- }
- .listRowBackground(Color.chart)
- Section {
- Toggle(isOn: $target_override) {
- Text("Override Target")
- }
- .onChange(of: target_override) {
- hasChanges = true
- }
- // Target Glucose Picker
- if target_override {
- let settingsProvider = PickerSettingsProvider.shared
- let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 72, max: 270, type: .glucose)
- TargetPicker(
- label: "Target Glucose",
- selection: Binding(
- get: { target ?? 100 },
- set: { target = $0 }
- ),
- options: settingsProvider.generatePickerValues(
- from: glucoseSetting,
- units: state.units,
- roundMinToStep: true
- ),
- units: state.units,
- hasChanges: $hasChanges,
- targetStep: $targetStep,
- displayPickerTarget: $displayPickerTarget,
- toggleScrollWheel: toggleScrollWheel
- )
- .onAppear {
- if target == 0 || target == nil {
- target = 100
- }
- }
- }
- }
- .listRowBackground(Color.chart)
- Section {
- // Picker for Disable SMB 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:
- smbIsOff = false
- smbIsScheduledOff = false
- case .disable:
- smbIsOff = true
- smbIsScheduledOff = false
- case .disableOnSchedule:
- smbIsOff = false
- smbIsScheduledOff = true
- }
- hasChanges = true
- }
- if smbIsScheduledOff {
- // First Hour SMBs Are Disabled
- HStack {
- Text("From")
- Spacer()
- Text(
- state.is24HourFormat() ? state.format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
- state.convertTo12HourFormat(Int(truncating: 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: end! as NSNumber)) + ":00" :
- state.convertTo12HourFormat(Int(truncating: end! as NSNumber))
- )
- .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
- .onTapGesture {
- displayPickerDisableSmbSchedule = toggleScrollWheel(displayPickerDisableSmbSchedule)
- }
- }
- if displayPickerDisableSmbSchedule {
- HStack {
- Picker(selection: Binding(
- get: { Int(truncating: start! as NSNumber) },
- set: {
- start = Decimal($0)
- hasChanges = true
- }
- ), label: Text("")) {
- if state.is24HourFormat() {
- ForEach(0 ..< 24, id: \.self) { hour in
- Text(state.format24Hour(hour) + ":00").tag(hour)
- }
- } else {
- ForEach(0 ..< 24, id: \.self) { hour in
- Text(state.convertTo12HourFormat(hour)).tag(hour)
- }
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- Picker(selection: Binding(
- get: { Int(truncating: end! as NSNumber) },
- set: {
- end = Decimal($0)
- hasChanges = true
- }
- ), label: Text("")) {
- if state.is24HourFormat() {
- ForEach(0 ..< 24, id: \.self) { hour in
- Text(state.format24Hour(hour) + ":00").tag(hour)
- }
- } else {
- ForEach(0 ..< 24, id: \.self) { hour in
- Text(state.convertTo12HourFormat(hour)).tag(hour)
- }
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- .listRowBackground(Color.chart)
- if !smbIsOff {
- Section {
- Toggle(isOn: $advancedSettings) {
- Text("Change Max SMB Minutes")
- }
- .onChange(of: advancedSettings) { hasChanges = true }
- if advancedSettings {
- // SMB Minutes Picker
- HStack {
- Text("SMB")
- Spacer()
- Text("\(smbMinutes?.formatted(.number) ?? "\(state.defaultSmbMinutes)") min")
- .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
- .onTapGesture {
- displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes)
- }
- Spacer()
- Divider().frame(width: 1, height: 20)
- Spacer()
- Text("UAM")
- Spacer()
- Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min")
- .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
- .onTapGesture {
- displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes)
- }
- }
- if displayPickerSmbMinutes {
- HStack {
- Picker(
- selection: Binding(
- get: { smbMinutes ?? state.defaultSmbMinutes },
- set: {
- smbMinutes = $0
- hasChanges = true
- }
- ),
- label: Text("")
- ) {
- ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
- Text("\(minute) min").tag(Decimal(minute))
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- Picker(
- selection: Binding(
- get: { uamMinutes ?? state.defaultUamMinutes },
- set: {
- uamMinutes = $0
- hasChanges = true
- }
- ),
- label: Text("")
- ) {
- ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
- Text("\(minute) min").tag(Decimal(minute))
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- .listRowBackground(Color.chart)
- }
- Section {
- Toggle(isOn: $indefinite) { Text("Enable Indefinitely") }
- .onChange(of: indefinite) { hasChanges = true }
- if !indefinite {
- HStack {
- Text("Duration")
- Spacer()
- Text(state.formatHrMin(Int(truncating: duration as NSNumber)))
- .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
- .onTapGesture {
- displayPickerDuration = toggleScrollWheel(displayPickerDuration)
- }
- }
- if displayPickerDuration {
- HStack {
- Picker(
- selection: Binding(
- get: {
- Int(truncating: duration as NSNumber) / 60
- },
- set: {
- let minutes = Int(truncating: duration as NSNumber) % 60
- let totalMinutes = $0 * 60 + minutes
- duration = Decimal(totalMinutes)
- hasChanges = true
- }
- ),
- label: Text("")
- ) {
- ForEach(0 ..< 24) { hour in
- Text("\(hour) hr").tag(hour)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- Picker(
- selection: Binding(
- get: {
- Int(truncating: duration as NSNumber) %
- 60 // Convert Decimal to Int for modulus operation
- },
- set: {
- duration = Decimal((Int(truncating: duration as NSNumber) / 60) * 60 + $0)
- hasChanges = true
- }
- ),
- label: Text("")
- ) {
- ForEach(Array(stride(from: 0, through: 55, by: 5)), id: \.self) { minute in
- Text("\(minute) min").tag(minute)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- .listRowBackground(Color.chart)
- }
- }
- private var saveButton: some View {
- let (isInvalid, errorMessage) = isOverrideInvalid()
- return Section(
- header:
- HStack {
- Spacer()
- Text(errorMessage ?? "").textCase(nil)
- .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
- Spacer()
- },
- content: {
- Button(action: {
- saveChanges()
- do {
- guard let moc = override.managedObjectContext else { return }
- guard moc.hasChanges else { return }
- try moc.save()
- Task {
- await state.nightscoutManager.uploadProfiles()
- }
- // Disable previous active Override
- if let currentActiveOverride = state.currentActiveOverride {
- Task {
- await state.disableAllActiveOverrides(
- except: currentActiveOverride.objectID,
- createOverrideRunEntry: false
- )
- // Update View
- state.updateLatestOverrideConfiguration()
- }
- }
- hasChanges = false
- presentationMode.wrappedValue.dismiss()
- } catch {
- debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
- }
- }, label: {
- Text("Save Override")
- })
- .disabled(isInvalid) // Disable button if changes are invalid
- .frame(maxWidth: .infinity, alignment: .center)
- .tint(.white)
- }
- )
- .listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
- }
- private func isOverrideInvalid() -> (Bool, String?) {
- let noDurationSpecified = !indefinite && duration == 0
- let targetZeroWithOverride = target_override && (target ?? 0 < 72 || target ?? 0 > 270)
- let allSettingsDefault = percentage == 100 && !target_override && !advancedSettings &&
- !smbIsOff && !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.")
- }
- if !hasChanges {
- return (true, nil)
- }
- return (false, nil)
- }
- private func saveChanges() {
- if !override.isPreset, hasChanges, name == (override.name ?? "") {
- override.name = "Custom Override"
- } else {
- override.name = name
- }
- override.percentage = percentage
- override.indefinite = indefinite
- override.duration = NSDecimalNumber(decimal: duration)
- override.target = target_override ? NSDecimalNumber(decimal: target ?? 100) : nil
- override.advancedSettings = advancedSettings
- override.smbIsOff = smbIsOff
- override.smbIsScheduledOff = smbIsScheduledOff
- override.start = start.map { NSDecimalNumber(decimal: $0) }
- override.end = end.map { NSDecimalNumber(decimal: $0) }
- override.isfAndCr = isfAndCr
- override.isf = isf
- override.cr = cr
- override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
- override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
- override.isUploadedToNS = false
- }
- private func resetValues() {
- name = override.name ?? ""
- percentage = override.percentage
- indefinite = override.indefinite
- duration = override.duration?.decimalValue ?? 0
- target = override.target?.decimalValue
- advancedSettings = override.advancedSettings
- smbIsOff = override.smbIsOff
- smbIsScheduledOff = override.smbIsScheduledOff
- start = override.start?.decimalValue
- end = override.end?.decimalValue
- isfAndCr = override.isfAndCr
- isf = override.isf
- cr = override.cr
- smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
- uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
- }
- private func toggleScrollWheel(_ toggle: Bool) -> Bool {
- displayPickerDuration = false
- displayPickerPercentage = false
- displayPickerTarget = false
- displayPickerDisableSmbSchedule = false
- displayPickerSmbMinutes = false
- return !toggle
- }
- }
|