Просмотр исходного кода

override UI updates

Edits to Override views:
* Changed from `Add Override` to `New Override` to encompass both creating a new preset as well as enacting a one-off custom override
* Changed to List from Form and added spacing
* Put `From` and `To` labels and pickers for `Disable [SMBs] on Schedule next to each other horizontally
* Put `Max SMB Basal Minutes` and `Max UAM SMB Basal Minutes` labels and pickers next to each other horizontally
* Turned `Percentage` and `Glucose Target` into a scroll wheel picker
* Changed `Enact Override` and `Save as Preset` to big colored buttons, similar to what's now in `tempTargets` branch
* Removed unused Alerts
* Default new target to 100 mg/dL
* Remove units conversion from target saving since everything is now saved in mg/dL
* Removed some `_ in` from Xcode warnings/suggestions since updating to iOS 17 minimum
* Changed `unChanged()` to `isOverrideInvalid()`, replaced checking `smbMinutes` against `defaultSmbMinutes` with checking `advancedSettings`
* Use `isOverrideInvalid()` to display a red footer when override is not in a valid state to save or enact
Mike Plante 1 год назад
Родитель
Сommit
ce7d009853

+ 1 - 3
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -130,9 +130,7 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             newOverride.enabled = override.enabled
             newOverride.smbIsOff = override.smbIsOff
             if override.overrideTarget {
-                newOverride.target = (
-                    self.settingsManager.settings.units == .mmolL ? override.target.asMgdL : override.target
-                ) as NSDecimalNumber
+                newOverride.target = override.target as NSDecimalNumber
             } else {
                 newOverride.target = 0
             }

+ 3 - 3
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -484,7 +484,7 @@ extension Home {
                                 NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
                         )
                         .font(.system(size: 16, weight: .bold, design: .rounded))
-                        .onChange(of: state.hours) { _ in
+                        .onChange(of: state.hours) {
                             state.roundedTotalBolus = state.calculateTINS()
                         }
                         .onAppear {
@@ -728,7 +728,7 @@ extension Home {
                 }
                 .background(color)
             }
-            .onChange(of: state.hours) { _ in
+            .onChange(of: state.hours) {
                 highlightButtons()
             }
             .onAppear {
@@ -898,7 +898,7 @@ extension Home {
                     }
                 )
             }.ignoresSafeArea(.keyboard, edges: .bottom).blur(radius: state.waitForSuggestion ? 8 : 0)
-                .onChange(of: selectedTab) { _ in
+                .onChange(of: selectedTab) {
                     print("current path is empty: \(settingsPath.isEmpty)")
                     settingsPath = NavigationPath()
                 }

+ 7 - 27
FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift

@@ -8,11 +8,11 @@ extension OverrideConfig {
         @Injected() var apsManager: APSManager!
         @Injected() var overrideStorage: OverrideStorage!
 
-        @Published var overrideSliderPercentage: Double = 100
+        @Published var overridePercentage: Double = 100
         @Published var isEnabled = false
         @Published var indefinite = true
         @Published var overrideDuration: Decimal = 0
-        @Published var target: Decimal = 0
+        @Published var target: Decimal = 100
         @Published var shouldOverrideTarget: Bool = false
         @Published var smbIsOff: Bool = false
         @Published var id = ""
@@ -34,7 +34,6 @@ extension OverrideConfig {
         @Published var activeOverrideName: String = ""
         @Published var currentActiveOverride: OverrideStored?
         @Published var showOverrideEditSheet = false
-        @Published var showInvalidTargetAlert = false
 
         var units: GlucoseUnits = .mgdL
 
@@ -70,24 +69,6 @@ extension OverrideConfig {
 
         let coredataContext = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-
-        func isInputInvalid(target: Decimal) -> Bool {
-            guard target != 0 else { return false }
-
-            if units == .mgdL,
-               target < 70 || target > 270
-            {
-                showInvalidTargetAlert = true
-                return true
-            } else if units == .mmolL,
-                      target < 4 || target > 15
-            {
-                showInvalidTargetAlert = true
-                return true
-            } else {
-                return false
-            }
-        }
     }
 }
 
@@ -223,7 +204,7 @@ extension OverrideConfig.StateModel {
             date: Date(),
             duration: overrideDuration,
             indefinite: indefinite,
-            percentage: overrideSliderPercentage,
+            percentage: overridePercentage,
             smbIsOff: smbIsOff,
             isPreset: isPreset,
             id: id,
@@ -262,7 +243,7 @@ extension OverrideConfig.StateModel {
             date: Date(),
             duration: overrideDuration,
             indefinite: indefinite,
-            percentage: overrideSliderPercentage,
+            percentage: overridePercentage,
             smbIsOff: smbIsOff,
             isPreset: true,
             id: id,
@@ -410,8 +391,7 @@ extension OverrideConfig.StateModel {
 
         overrideDuration = 0
         indefinite = true
-        overrideSliderPercentage = 100
-
+        overridePercentage = 100
         advancedSettings = false
         smbIsOff = false
         overrideName = ""
@@ -424,7 +404,7 @@ extension OverrideConfig.StateModel {
         end = 0
         smbMinutes = defaultSmbMinutes
         uamMinutes = defaultUamMinutes
-        target = 0
+        target = 100
     }
 }
 
@@ -466,7 +446,7 @@ extension OverrideConfig.StateModel {
         var highTarget = lowTarget
 
         if units == .mmolL, !viewPercantage {
-            lowTarget = Decimal(round(Double(lowTarget.asMgdL)))
+            lowTarget = Decimal(round(Double(lowTarget)))
             highTarget = lowTarget
         }
 

+ 228 - 266
FreeAPS/Sources/Modules/OverrideConfig/View/AddOverrideForm.swift

@@ -6,17 +6,16 @@ struct AddOverrideForm: View {
     @StateObject var state: OverrideConfig.StateModel
     @State private var selectedIsfCrOption: isfAndOrCrOptions = .isfAndCr
     @State private var selectedDisableSmbOption: disableSmbOptions = .dontDisable
+    @State private var displayPickerPercentage: Bool = false
     @State private var displayPickerDuration: Bool = false
-    @State private var displayPickerStart: Bool = false
-    @State private var displayPickerEnd: Bool = false
+    @State private var displayPickerTarget: Bool = false
+    @State private var displayPickerDisableSmbSchedule: Bool = false
     @State private var displayPickerSmbMinutes: Bool = false
-    @State private var displayPickerUamMinutes: Bool = false
     @State private var durationHours = 0
     @State private var durationMinutes = 0
     @State private var overrideTarget = false
+    @State private var didPressSave = false
     @Environment(\.colorScheme) var colorScheme
-    @State private var showAlert = false
-    @State private var alertString = ""
 
     @Environment(\.dismiss) var dismiss
 
@@ -41,8 +40,7 @@ struct AddOverrideForm: View {
             ]),
             startPoint: .top,
             endPoint: .bottom
-        )
-            :
+        ) :
             LinearGradient(
                 gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
                 startPoint: .top,
@@ -68,88 +66,114 @@ struct AddOverrideForm: View {
         return formatter
     }
 
-    private var alertMessage: String {
-        let target: String = state.units == .mgdL ? "70-270 mg/dl" : "4-15 mmol/l"
-        return "Please enter a valid target between" + " \(target)."
-    }
-
     var body: some View {
         NavigationView {
-            Form {
+            List {
                 addOverride()
-            }.scrollContentBackground(.hidden).background(color)
-                .navigationTitle("Add Override")
-                .navigationBarItems(trailing: Button("Cancel") {
-                    presentationMode.wrappedValue.dismiss()
-                })
+                saveButton
+            }
+            .listSectionSpacing(20)
+            .listRowSpacing(10)
+            .scrollContentBackground(.hidden).background(color)
+            .navigationTitle("New Override")
+            .navigationBarItems(trailing: Button("Cancel") {
+                presentationMode.wrappedValue.dismiss()
+            })
         }
     }
 
     @ViewBuilder private func addOverride() -> some View {
         Section {
+            let pad: CGFloat = 3
             VStack {
                 HStack {
                     Text("Name")
                     Spacer()
                     TextField("(Optional)", text: $state.overrideName).multilineTextAlignment(.trailing)
                 }
+                .padding(.vertical, pad)
             }
 
             VStack {
-                HStack {
-                    Spacer()
-
-                    // Decrement button
-                    Button(action: {
-                        if state.overrideSliderPercentage > 10 {
-                            state.overrideSliderPercentage -= 1
-                        }
-                    }) {
-                        Image(systemName: "minus.circle.fill")
-                            .font(.title)
-                            .foregroundColor(state.overrideSliderPercentage > 10 ? .accentColor : .loopGray)
+                Toggle(isOn: $state.indefinite) {
+                    Text("Enable Indefinitely")
+                }
+                .padding(.vertical, pad)
+                if !state.indefinite {
+                    HStack {
+                        Text("Duration")
+                        Spacer()
+                        Text(formatHrMin(Int(state.overrideDuration)))
+                            .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
+                    }
+                    .padding(.vertical, pad)
+                    .onTapGesture {
+                        displayPickerDuration.toggle()
                     }
-                    .buttonStyle(PlainButtonStyle())
-
-                    Spacer()
-
-                    Text("\(Int(state.overrideSliderPercentage)) %")
-                        .font(.largeTitle)
-                        .foregroundColor(.accentColor)
 
-                    Spacer()
+                    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 = Decimal(totalDurationInMinutes())
+                            }
 
-                    // Increment button
-                    Button(action: {
-                        if state.overrideSliderPercentage < 200 {
-                            state.overrideSliderPercentage += 1
+                            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 = Decimal(totalDurationInMinutes())
+                            }
                         }
-                    }) {
-                        Image(systemName: "plus.circle.fill")
-                            .font(.title)
-                            .foregroundColor(state.overrideSliderPercentage < 200 ? .accentColor : .loopGray)
                     }
-                    .buttonStyle(PlainButtonStyle())
+                }
+            }
 
+            VStack {
+                // Percentage Picker
+                HStack {
+                    Text("Change Basal Rate by")
                     Spacer()
+                    Text("\(state.overridePercentage.formatted(.number)) %")
+                        .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)
+                }
+                .padding(.vertical, pad)
+                .onTapGesture {
+                    displayPickerPercentage.toggle()
                 }
-                .padding()
 
-                // Slider to adjust value
-                Slider(
-                    value: $state.overrideSliderPercentage,
-                    in: 10 ... 200,
-                    step: 1
-                )
+                if displayPickerPercentage {
+                    Picker(selection: Binding(
+                        get: { Int(truncating: state.overridePercentage as NSNumber) },
+                        set: { state.overridePercentage = Double($0) }
+                    ), label: Text("")) {
+                        ForEach(Array(stride(from: 10, through: 200, by: 5)), id: \.self) { percent in
+                            Text("\(percent) %").tag(percent)
+                        }
+                    }
+                    .pickerStyle(WheelPickerStyle())
+                    .frame(maxWidth: .infinity)
+                }
 
                 // Picker for ISF/CR settings
-                Picker("Apply to", selection: $selectedIsfCrOption) {
+                Picker("Also Inversely Change", selection: $selectedIsfCrOption) {
                     ForEach(isfAndOrCrOptions.allCases, id: \.self) { option in
                         Text(option.rawValue).tag(option)
                     }
                 }
+                .padding(.top, pad)
                 .pickerStyle(MenuPickerStyle())
-                .onChange(of: selectedIsfCrOption) { newValue in
+                .onChange(of: selectedIsfCrOption) { _, newValue in
                     switch newValue {
                     case .isfAndCr:
                         state.isfAndCr = true
@@ -172,61 +196,42 @@ struct AddOverrideForm: View {
             }
 
             VStack {
-                Toggle(isOn: $state.indefinite) {
-                    Text("Enable Indefinitely")
+                Toggle(isOn: $state.shouldOverrideTarget) {
+                    Text("Override Profile Target")
                 }
-                if !state.indefinite {
+                .padding(.vertical, pad)
+                if state.shouldOverrideTarget {
                     VStack {
                         HStack {
-                            Text("Duration")
+                            Text("Target Glucose")
                             Spacer()
-                            Text(formatHrMin(Int(state.overrideDuration)))
-                                .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
+                            Text(formattedGlucose(glucose: state.target))
+                                .foregroundColor(!displayPickerTarget ? .primary : .accentColor)
                         }
+                        .padding(.vertical, pad)
                         .onTapGesture {
-                            displayPickerDuration.toggle()
+                            displayPickerTarget.toggle()
                         }
 
-                        if displayPickerDuration {
-                            HStack {
-                                Picker("Hours", selection: $durationHours) {
-                                    ForEach(0 ..< 24) { hour in
-                                        Text("\(hour) hr").tag(hour)
-                                    }
-                                }
-                                .pickerStyle(WheelPickerStyle())
-                                .frame(width: 100)
-                                .onChange(of: durationHours) { _ in
-                                    state.overrideDuration = Decimal(totalDurationInMinutes())
-                                }
-
-                                Picker("Minutes", selection: $durationMinutes) {
-                                    ForEach(Array(stride(from: 0, through: 55, by: 5)), id: \.self) { minute in
-                                        Text("\(minute) min").tag(minute)
-                                    }
+                        if displayPickerTarget {
+                            let step = state.units == .mgdL ? 1 : 2
+                            Picker(selection: Binding(
+                                get: { Int(truncating: state.target as NSNumber) },
+                                set: { state.target = Decimal($0)
                                 }
-                                .pickerStyle(WheelPickerStyle())
-                                .frame(width: 100)
-                                .onChange(of: durationMinutes) { _ in
-                                    state.overrideDuration = Decimal(totalDurationInMinutes())
+                            ), label: Text("")) {
+                                ForEach(
+                                    Array(stride(from: 72, through: 270, by: step)),
+                                    id: \.self
+                                ) { glucose in
+                                    Text(formattedGlucose(glucose: Decimal(glucose)))
+                                        .tag(glucose)
                                 }
                             }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
                         }
                     }
-                    .padding(.top)
-                }
-            }
-
-            VStack {
-                Toggle(isOn: $state.shouldOverrideTarget) {
-                    Text("Override Profile Target")
-                }
-                if state.shouldOverrideTarget {
-                    HStack {
-                        Text("Target Glucose")
-                        TextFieldWithToolBar(text: $state.target, placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
                 }
             }
 
@@ -237,8 +242,9 @@ struct AddOverrideForm: View {
                         Text(option.rawValue).tag(option)
                     }
                 }
+                .padding(.vertical, pad)
                 .pickerStyle(MenuPickerStyle())
-                .onChange(of: selectedDisableSmbOption) { newValue in
+                .onChange(of: selectedDisableSmbOption) { _, newValue in
                     switch newValue {
                     case .dontDisable:
                         state.smbIsOff = false
@@ -258,63 +264,57 @@ struct AddOverrideForm: View {
                         HStack {
                             Text("From")
                             Spacer()
-
                             Text(
                                 is24HourFormat() ? format24Hour(Int(truncating: state.start as NSNumber)) + ":00" :
                                     convertTo12HourFormat(Int(truncating: state.start as NSNumber))
                             )
-                            .foregroundColor(!displayPickerStart ? .primary : .accentColor)
-                        }
-                        .onTapGesture {
-                            displayPickerStart.toggle()
-                        }
+                            .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
 
-                        if displayPickerStart {
-                            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(is24HourFormat() ? format24Hour(hour) + ":00" : convertTo12HourFormat(hour))
-                                        .tag(hour)
-                                }
-                            }
-                            .pickerStyle(WheelPickerStyle())
-                            .frame(maxWidth: .infinity)
-                        }
-                    }
-                    .padding(.top, 10)
-
-                    // First Hour SMBs Are Resumed
-                    VStack {
-                        HStack {
+                            Divider().frame(width: 1, height: 20)
                             Text("To")
                             Spacer()
                             Text(
                                 is24HourFormat() ? format24Hour(Int(truncating: state.end as NSNumber)) + ":00" :
                                     convertTo12HourFormat(Int(truncating: state.end as NSNumber))
                             )
-                            .foregroundColor(!displayPickerEnd ? .primary : .accentColor)
+                            .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
+                            Spacer()
                         }
+                        .padding(.vertical, pad)
                         .onTapGesture {
-                            displayPickerEnd.toggle()
+                            displayPickerDisableSmbSchedule.toggle()
                         }
 
-                        if displayPickerEnd {
-                            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(is24HourFormat() ? format24Hour(hour) + ":00" : convertTo12HourFormat(hour))
-                                        .tag(hour)
+                        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(is24HourFormat() ? format24Hour(hour) + ":00" : 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(is24HourFormat() ? format24Hour(hour) + ":00" : convertTo12HourFormat(hour))
+                                            .tag(hour)
+                                    }
                                 }
+                                .pickerStyle(WheelPickerStyle())
+                                .frame(maxWidth: .infinity)
                             }
-                            .pickerStyle(WheelPickerStyle())
-                            .frame(maxWidth: .infinity)
                         }
                     }
-                    .padding(.vertical, 10)
                 }
             }
 
@@ -323,160 +323,101 @@ struct AddOverrideForm: View {
                     Toggle(isOn: $state.advancedSettings) {
                         Text("Override Max SMB Minutes")
                     }
+                    .padding(.vertical, pad)
 
                     if state.advancedSettings {
                         // SMB Minutes Picker
                         VStack {
                             HStack {
-                                Text("Max SMB Minutes")
+                                Text("SMB")
                                 Spacer()
-
                                 Text("\(state.smbMinutes.formatted(.number)) min")
                                     .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
+                                Divider().frame(width: 1, height: 20)
+                                Text("UAM")
+                                Spacer()
+                                Text("\(state.uamMinutes.formatted(.number)) min")
+                                    .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
                             }
+                            .padding(.vertical, pad)
                             .onTapGesture {
                                 displayPickerSmbMinutes.toggle()
                             }
 
                             if displayPickerSmbMinutes {
-                                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)
+                                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)
-                            }
-                        }
-                        .padding(.top)
-
-                        // UAM SMB Minutes Picker
-                        VStack {
-                            HStack {
-                                Text("Max UAM SMB Minutes")
-                                Spacer()
-                                Text("\(state.uamMinutes.formatted(.number)) min")
-                                    .foregroundColor(!displayPickerUamMinutes ? .primary : .accentColor)
-                            }
-                            .onTapGesture {
-                                displayPickerUamMinutes.toggle()
-                            }
-
-                            if displayPickerUamMinutes {
-                                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)
+
+                                    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)
                                 }
-                                .pickerStyle(WheelPickerStyle())
-                                .frame(maxWidth: .infinity)
                             }
                         }
-                        .padding(.top)
                     }
                 }
             }
-
-            startAndSaveProfiles
         }
-        header: { Text("Add custom Override") }
-        footer: {
-            Text(
-                "Your profile ISF and CR will be inversely adjusted with the override percentage."
-            )
-        }.listRowBackground(Color.chart)
+        .listRowBackground(Color.chart)
     }
 
-    private var startAndSaveProfiles: some View {
-        HStack {
-            Button("Start New Override") {
-                if !state.isInputInvalid(target: state.target) {
-                    showAlert.toggle()
-
-                    alertString = "\(state.overrideSliderPercentage.formatted(.number)) %, " +
-                        (
-                            state.overrideDuration > 0 || !state
-                                .indefinite ?
-                                (
-                                    state
-                                        .overrideDuration
-                                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
-                                        " min."
-                                ) :
-                                NSLocalizedString(" infinite duration.", comment: "")
-                        ) +
-                        (
-                            (state.target == 0 || !state.shouldOverrideTarget) ? "" :
-                                (" Target: " + state.target.formatted() + " " + state.units.rawValue + ".")
-                        )
-                        +
-                        (
-                            state
-                                .smbIsOff ?
-                                NSLocalizedString(
-                                    " SMBs are disabled either by schedule or during the entire duration.",
-                                    comment: ""
-                                ) : ""
-                        )
-                        +
-                        "\n\n"
-                        +
-                        NSLocalizedString(
-                            "Starting this override will change your profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Override” will start your new Override or edit your current active Override.",
-                            comment: ""
-                        )
-                }
-            }
-            .disabled(unChanged())
-            .buttonStyle(BorderlessButtonStyle())
-            .font(.callout)
-            .controlSize(.mini)
-            .alert(
-                "Start Override",
-                isPresented: $showAlert,
-                actions: {
-                    Button("Cancel", role: .cancel) { state.isEnabled = false }
-                    Button("Start Override", role: .destructive) {
-                        Task {
-                            if state.indefinite { state.overrideDuration = 0 }
-                            state.isEnabled.toggle()
-                            await state.saveCustomOverride()
-                            await state.resetStateVariables()
-                            dismiss()
-                        }
+    private var saveButton: some View {
+        let (isInvalid, errorMessage) = isOverrideInvalid()
+
+        return Group {
+            Section {
+                Button(action: {
+                    Task {
+                        if state.indefinite { state.overrideDuration = 0 }
+                        state.isEnabled.toggle()
+                        await state.saveCustomOverride()
+                        await state.resetStateVariables()
+                        dismiss()
                     }
-                },
-                message: {
-                    Text(alertString)
-                }
-            )
-            .alert(isPresented: $state.showInvalidTargetAlert) {
-                Alert(
-                    title: Text("Invalid Input"),
-                    message: Text("\(state.alertMessage)"),
-                    dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
-                )
-            }
-            Button {
-                Task {
-                    if !state.isInputInvalid(target: state.target) {
+                }, label: {
+                    Text("Enact Override")
+                })
+                    .disabled(isInvalid)
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .tint(.white)
+            }.listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
+
+            Section(
+                footer: Text(errorMessage ?? "")
+                    .foregroundColor(.red)
+            ) {
+                Button(action: {
+                    Task {
                         await state.saveOverridePreset()
                         dismiss()
                     }
-                }
+                }, label: {
+                    Text("Save as Preset")
+
+                })
+                    .disabled(isInvalid)
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .tint(.white)
             }
-            label: { Text("Save as Preset") }
-                .tint(.orange)
-                .frame(maxWidth: .infinity, alignment: .trailing)
-                .buttonStyle(BorderlessButtonStyle())
-                .controlSize(.mini)
-                .disabled(unChanged())
+            .listRowBackground(
+                isInvalid ? Color(.systemGray4) : Color(.orange)
+            )
         }
     }
 
@@ -485,14 +426,35 @@ struct AddOverrideForm: View {
         return max(0, durationTotal)
     }
 
-    private func unChanged() -> Bool {
-        let defaultProfile = state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.advancedSettings
+    private func isOverrideInvalid() -> (Bool, String?) {
         let noDurationSpecified = !state.indefinite && state.overrideDuration == 0
         let targetZeroWithOverride = state.shouldOverrideTarget && state.target == 0
-        let allSettingsDefault = state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.smbIsOff && !state
-            .smbIsScheduledOff && state.smbMinutes == state.defaultSmbMinutes && state.uamMinutes == state.defaultUamMinutes
+        let allSettingsDefault = state.overridePercentage == 100 && !state.shouldOverrideTarget &&
+            !state.advancedSettings && !state.smbIsOff && !state.smbIsScheduledOff
 
-        return defaultProfile || noDurationSpecified || targetZeroWithOverride || allSettingsDefault
+        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)
+    }
+
+    private func formattedGlucose(glucose: Decimal) -> String {
+        let formattedValue: String
+        if state.units == .mgdL {
+            formattedValue = glucoseFormatter.string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
+        } else {
+            formattedValue = glucose.formattedAsMmolL
+        }
+        return "\(formattedValue) \(state.units.rawValue)"
     }
 }
 

+ 294 - 258
FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift

@@ -28,12 +28,11 @@ struct EditOverrideForm: View {
     @State private var hasChanges = false
     @State private var isEditing = false
     @State private var target_override = false
-    @State private var showAlert = false
+    @State private var displayPickerPercentage: Bool = false
     @State private var displayPickerDuration: Bool = false
-    @State private var displayPickerStart: Bool = false
-    @State private var displayPickerEnd: Bool = false
+    @State private var displayPickerTarget: Bool = false
+    @State private var displayPickerDisableSmbSchedule: Bool = false
     @State private var displayPickerSmbMinutes: Bool = false
-    @State private var displayPickerUamMinutes: Bool = false
 
     init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) {
         override = overrideToEdit
@@ -42,10 +41,7 @@ struct EditOverrideForm: View {
         _percentage = State(initialValue: overrideToEdit.percentage)
         _indefinite = State(initialValue: overrideToEdit.indefinite)
         _duration = State(initialValue: overrideToEdit.duration?.decimalValue ?? 0)
-        _target = State(
-            initialValue: state.units == .mgdL ? overrideToEdit.target?.decimalValue : overrideToEdit.target?
-                .decimalValue.asMmolL
-        )
+        _target = State(initialValue: overrideToEdit.target?.decimalValue)
         _target_override = State(initialValue: overrideToEdit.target?.decimalValue != 0)
         _advancedSettings = State(initialValue: overrideToEdit.advancedSettings)
         _smbIsOff = State(initialValue: overrideToEdit.smbIsOff)
@@ -116,100 +112,147 @@ struct EditOverrideForm: View {
 
     var body: some View {
         NavigationView {
-            Form {
+            List {
                 editOverride()
-
                 saveButton
-
-            }.scrollContentBackground(.hidden).background(color)
-                .navigationTitle("Edit Override")
-                .navigationBarTitleDisplayMode(.inline)
-                .navigationBarItems(leading: Button("Close") {
-                    presentationMode.wrappedValue.dismiss()
-                })
-                .onDisappear {
-                    if !hasChanges {
-                        // Reset UI changes
-                        resetValues()
-                    }
-                }
-                .alert(isPresented: $state.showInvalidTargetAlert) {
-                    Alert(
-                        title: Text("Invalid Input"),
-                        message: Text("\(state.alertMessage)"),
-                        dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
-                    )
+            }
+            .listSectionSpacing(20)
+            .listRowSpacing(10)
+            .scrollContentBackground(.hidden).background(color)
+            .navigationTitle("Edit Override")
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationBarItems(trailing: Button("Cancel") {
+                presentationMode.wrappedValue.dismiss()
+            })
+            .onDisappear {
+                if !hasChanges {
+                    // Reset UI changes
+                    resetValues()
                 }
+            }
         }
     }
 
     @ViewBuilder private func editOverride() -> some View {
         Section {
+            let pad: CGFloat = 3
             if override.name != nil {
                 VStack {
                     HStack {
                         Text("Name")
                         Spacer()
                         TextField("Name", text: $name)
-                            .onChange(of: name) { _ in hasChanges = true }
+                            .onChange(of: name) { hasChanges = true }
                             .multilineTextAlignment(.trailing)
                     }
+                    .padding(.vertical, pad)
                 }
             }
+
             VStack {
-                HStack {
-                    Spacer()
+                Toggle(isOn: $indefinite) { Text("Enable Indefinitely") }
+                    .padding(.vertical, pad)
+                    .onChange(of: indefinite) { hasChanges = true }
 
-                    // Decrement button
-                    Button(action: {
-                        if percentage > 10 {
-                            percentage -= 1
-                        }
-                    }) {
-                        Image(systemName: "minus.circle.fill")
-                            .font(.title)
-                            .foregroundColor(percentage > 10 ? .accentColor : .loopGray)
+                if !indefinite {
+                    HStack {
+                        Text("Duration")
+                        Spacer()
+                        Text(formatHrMin(Int(truncating: duration as NSNumber)))
+                            .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
+                    }
+                    .padding(.vertical, pad)
+                    .onTapGesture {
+                        displayPickerDuration.toggle()
                     }
-                    .buttonStyle(PlainButtonStyle())
-
-                    Spacer()
-
-                    Text("\(percentage.formatted(.number)) %")
-                        .foregroundColor(.accentColor)
-                        .font(.largeTitle)
 
-                    Spacer()
+                    if displayPickerDuration {
+                        HStack {
+                            Picker(
+                                selection: Binding(
+                                    get: {
+                                        Int(truncating: duration as NSNumber) / 60
+                                    },
+                                    set: {
+                                        duration = Decimal($0 * 60 + Int(truncating: duration as NSNumber) % 60)
+                                        hasChanges = true
+                                    }
+                                ),
+                                label: Text("")
+                            ) {
+                                ForEach(0 ..< 24) { hour in
+                                    Text("\(hour) hr").tag(hour)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
 
-                    // Increment button
-                    Button(action: {
-                        if percentage < 200 {
-                            percentage += 1
+                            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)
                         }
-                    }) {
-                        Image(systemName: "plus.circle.fill")
-                            .font(.title)
-                            .foregroundColor(percentage < 200 ? .accentColor : .loopGray)
                     }
-                    .buttonStyle(PlainButtonStyle())
+                }
+            }
 
+            // Percentage Picker
+            VStack {
+                HStack {
+                    Text("Change Basal Rate by")
                     Spacer()
+                    Text("\(percentage.formatted(.number)) %")
+                        .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)
+                }
+                .padding(.vertical, pad)
+                .onTapGesture {
+                    displayPickerPercentage.toggle()
                 }
-                .padding()
 
-                Slider(
-                    value: $percentage,
-                    in: 10 ... 200,
-                    step: 1
-                ).onChange(of: percentage) { _ in hasChanges = true }
+                if displayPickerPercentage {
+                    Picker(
+                        selection: Binding(
+                            get: { max(10, min(floor(percentage / 5) * 5, 200)) },
+                            // round down to nearest multiple of 5 and limit from 10-200
+                            set: {
+                                percentage = $0
+                                hasChanges = true
+                            }
+                        ),
+                        label: Text("")
+                    ) {
+                        ForEach(Array(stride(from: 10.0, through: 200.0, by: 5.0)), id: \.self) { percent in
+                            Text("\(Int(percent)) %").tag(percent)
+                        }
+                    }
+                    .pickerStyle(WheelPickerStyle())
+                    .frame(maxWidth: .infinity)
+                }
 
                 // Picker for ISF/CR settings
-                Picker("Apply to", selection: $selectedIsfCrOption) {
+                Picker("Also Change", selection: $selectedIsfCrOption) {
                     ForEach(isfAndOrCrOptions.allCases, id: \.self) { option in
                         Text(option.rawValue).tag(option)
                     }
                 }
+                .padding(.top, pad)
                 .pickerStyle(MenuPickerStyle())
-                .onChange(of: selectedIsfCrOption) { newValue in
+                .onChange(of: selectedIsfCrOption) { _, newValue in
                     switch newValue {
                     case .isfAndCr:
                         isfAndCr = true
@@ -233,89 +276,26 @@ struct EditOverrideForm: View {
             }
 
             VStack {
-                Toggle(isOn: $indefinite) {
-                    Text("Enable Indefinitely")
-                }.onChange(of: indefinite) { _ in hasChanges = true }
-
-                if !indefinite {
-                    VStack {
-                        HStack {
-                            Text("Duration")
-                            Spacer()
-                            Text(formatHrMin(Int(truncating: duration as NSNumber)))
-                                .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
-                        }
-                        .onTapGesture {
-                            displayPickerDuration.toggle()
-                        }
-
-                        if displayPickerDuration {
-                            HStack {
-                                Picker(
-                                    selection: Binding(
-                                        get: {
-                                            Int(truncating: duration as NSNumber) / 60
-                                        },
-                                        set: {
-                                            duration = Decimal($0 * 60 + Int(truncating: duration as NSNumber) % 60)
-                                            hasChanges = true
-                                        }
-                                    ),
-                                    label: Text("")
-                                ) {
-                                    ForEach(0 ..< 24) { hour in
-                                        Text("\(hour) hr").tag(hour)
-                                    }
-                                }
-                                .pickerStyle(WheelPickerStyle())
-                                .frame(width: 100)
-
-                                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(width: 100)
-                            }
-                        }
-                    }
-                    .padding(.top)
-                }
-            }
-
-            VStack {
                 Toggle(isOn: $target_override) {
                     Text("Override Target")
-                }.onChange(of: target_override) { _ in
+                }
+                .padding(.vertical, pad)
+                .onChange(of: target_override) {
                     hasChanges = true
                 }
+                // Target Glucose Picker
                 if target_override {
-                    HStack {
-                        Text("Target Glucose")
-                        TextFieldWithToolBar(text: Binding(
-                            get: {
-                                target ?? 0
-                            },
-                            set: {
-                                target = $0
-                                hasChanges = true
-                            }
-                        ), placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
+                    let step: Decimal = state.units == .mgdL ? 1 : 2
+                    ScrollWheelPicker(
+                        label: "Target Glucose",
+                        selection: Binding(
+                            get: { target ?? Decimal(100) },
+                            set: { target = $0 }
+                        ),
+                        options: Array(stride(from: Decimal(72), through: Decimal(270), by: step)),
+                        formatter: { formattedGlucose(glucose: $0) },
+                        hasChanges: $hasChanges
+                    )
                 }
             }
 
@@ -326,8 +306,9 @@ struct EditOverrideForm: View {
                         Text(option.rawValue).tag(option)
                     }
                 }
+                .padding(.vertical, pad)
                 .pickerStyle(MenuPickerStyle())
-                .onChange(of: selectedDisableSmbOption) { newValue in
+                .onChange(of: selectedDisableSmbOption) { _, newValue in
                     switch newValue {
                     case .dontDisable:
                         smbIsOff = false
@@ -344,22 +325,31 @@ struct EditOverrideForm: View {
 
                 if smbIsScheduledOff {
                     // First Hour SMBs Are Disabled
-                    VStack {
-                        HStack {
-                            Text("From")
-                            Spacer()
-
-                            Text(
-                                is24HourFormat() ? format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
-                                    convertTo12HourFormat(Int(truncating: start! as NSNumber))
-                            )
-                            .foregroundColor(!displayPickerStart ? .primary : .accentColor)
-                        }
-                        .onTapGesture {
-                            displayPickerStart.toggle()
-                        }
+                    HStack {
+                        Text("From")
+                        Spacer()
+                        Text(
+                            is24HourFormat() ? format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
+                                convertTo12HourFormat(Int(truncating: start! as NSNumber))
+                        )
+                        .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
+
+                        Divider().frame(width: 1, height: 20)
+                        Text("To")
+                        Spacer()
+                        Text(
+                            is24HourFormat() ? format24Hour(Int(truncating: end! as NSNumber)) + ":00" :
+                                convertTo12HourFormat(Int(truncating: end! as NSNumber))
+                        )
+                        .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
+                    }
+                    .padding(.vertical, pad)
+                    .onTapGesture {
+                        displayPickerDisableSmbSchedule.toggle()
+                    }
 
-                        if displayPickerStart {
+                    if displayPickerDisableSmbSchedule {
+                        HStack {
                             Picker(selection: Binding(
                                 get: { Int(truncating: start! as NSNumber) },
                                 set: {
@@ -379,27 +369,7 @@ struct EditOverrideForm: View {
                             }
                             .pickerStyle(WheelPickerStyle())
                             .frame(maxWidth: .infinity)
-                        }
-                    }
-                    .padding(.top)
-
-                    // First Hour SMBs Are Resumed
-                    VStack {
-                        HStack {
-                            Text("To")
-                            Spacer()
-
-                            Text(
-                                is24HourFormat() ? format24Hour(Int(truncating: end! as NSNumber)) + ":00" :
-                                    convertTo12HourFormat(Int(truncating: end! as NSNumber))
-                            )
-                            .foregroundColor(!displayPickerEnd ? .primary : .accentColor)
-                        }
-                        .onTapGesture {
-                            displayPickerEnd.toggle()
-                        }
 
-                        if displayPickerEnd {
                             Picker(selection: Binding(
                                 get: { Int(truncating: end! as NSNumber) },
                                 set: {
@@ -421,7 +391,6 @@ struct EditOverrideForm: View {
                             .frame(maxWidth: .infinity)
                         }
                     }
-                    .padding(.top)
                 }
             }
 
@@ -429,119 +398,148 @@ struct EditOverrideForm: View {
                 VStack {
                     Toggle(isOn: $advancedSettings) {
                         Text("Change Max SMB Minutes")
-                    }.onChange(of: advancedSettings) { _ in hasChanges = true }
+                    }
+                    .padding(.vertical, pad)
+                    .onChange(of: advancedSettings) { hasChanges = true }
 
                     if advancedSettings {
                         // SMB Minutes Picker
                         VStack {
                             HStack {
-                                Text("Max SMB Minutes")
+                                Text("SMB")
                                 Spacer()
                                 Text("\(smbMinutes?.formatted(.number) ?? "\(state.defaultSmbMinutes)") min")
                                     .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
+                                Divider().frame(width: 1, height: 20)
+                                Text("UAM")
+                                Spacer()
+                                Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min")
+                                    .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
                             }
+                            .padding(.vertical, pad)
                             .onTapGesture {
                                 displayPickerSmbMinutes.toggle()
                             }
 
                             if displayPickerSmbMinutes {
-                                Picker(
-                                    selection: Binding(
-                                        get: { smbMinutes ?? state.defaultSmbMinutes },
-                                        set: {
-                                            smbMinutes = $0
-                                            hasChanges = true
+                                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))
                                         }
-                                    ),
-                                    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)
-                            }
-                        }
-                        .padding(.top)
-
-                        // UAM SMB Minutes Picker
-                        VStack {
-                            HStack {
-                                Text("Max UAM SMB Minutes")
-                                Spacer()
-                                Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min")
-                                    .foregroundColor(!displayPickerUamMinutes ? .primary : .accentColor)
-                            }
-                            .onTapGesture {
-                                displayPickerUamMinutes.toggle()
-                            }
-
-                            if displayPickerUamMinutes {
-                                Picker(
-                                    selection: Binding(
-                                        get: { uamMinutes ?? state.defaultUamMinutes },
-                                        set: {
-                                            uamMinutes = $0
-                                            hasChanges = true
+                                    .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))
                                         }
-                                    ),
-                                    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)
                                 }
-                                .pickerStyle(WheelPickerStyle())
-                                .frame(maxWidth: .infinity)
                             }
                         }
-                        .padding(.top)
                     }
                 }
             }
-
-        }.listRowBackground(Color.chart)
+        }
+        .listRowBackground(Color.chart)
     }
 
     private var saveButton: some View {
-        HStack {
-            Spacer()
+        let (isInvalid, errorMessage) = isOverrideInvalid()
+
+        return Section(
+            footer: Text(errorMessage ?? "")
+                .foregroundColor(.red)
+        ) {
             Button(action: {
-                if !state.isInputInvalid(target: target ?? 0) {
-                    saveChanges()
-
-                    do {
-                        guard let moc = override.managedObjectContext else { return }
-                        guard moc.hasChanges else { return }
-                        try moc.save()
-
-                        if let currentActiveOverride = state.currentActiveOverride {
-                            Task {
-                                await state.disableAllActiveOverrides(
-                                    except: currentActiveOverride.objectID,
-                                    createOverrideRunEntry: false
-                                )
-                            }
+                saveChanges()
+
+                do {
+                    guard let moc = override.managedObjectContext else { return }
+                    guard moc.hasChanges else { return }
+                    try moc.save()
+
+                    if let currentActiveOverride = state.currentActiveOverride {
+                        Task {
+                            await state.disableAllActiveOverrides(
+                                except: currentActiveOverride.objectID,
+                                createOverrideRunEntry: false
+                            )
+                            // Update View
+                            state.updateLatestOverrideConfiguration()
                         }
-
-                        // Update View
-                        state.updateLatestOverrideConfiguration()
-                        hasChanges = false
-                        presentationMode.wrappedValue.dismiss()
-                    } catch {
-                        debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
                     }
+
+                    hasChanges = false
+                    presentationMode.wrappedValue.dismiss()
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
                 }
             }, label: {
-                Text("Save")
+                Text("Save Override")
             })
-                .disabled(!hasChanges || (!indefinite && duration == 0))
+                .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
 
-            Spacer()
-        }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
+        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 formattedGlucose(glucose: Decimal) -> String {
+        let formattedValue: String
+        if state.units == .mgdL {
+            formattedValue = glucoseFormatter.string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
+        } else {
+            formattedValue = glucose.formattedAsMmolL
+        }
+        return "\(formattedValue) \(state.units.rawValue)"
     }
 
     private func saveChanges() {
@@ -591,3 +589,41 @@ struct EditOverrideForm: View {
         uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
     }
 }
+
+struct ScrollWheelPicker<T: Hashable>: View {
+    let label: String
+    @Binding var selection: T
+    let options: [T]
+    let formatter: (T) -> String
+    @Binding var hasChanges: Bool
+    @State private var isDisplayed: Bool = false
+
+    var body: some View {
+        VStack {
+            HStack {
+                Text(label)
+                Spacer()
+                Text(formatter(selection))
+                    .foregroundColor(.accentColor)
+            }
+            .onTapGesture {
+                isDisplayed.toggle()
+            }
+            if isDisplayed {
+                Picker(selection: Binding(
+                    get: { selection },
+                    set: {
+                        selection = $0
+                        hasChanges = true
+                    }
+                ), label: Text("")) {
+                    ForEach(options, id: \.self) { option in
+                        Text(formatter(option)).tag(option)
+                    }
+                }
+                .pickerStyle(WheelPickerStyle())
+                .frame(maxWidth: .infinity)
+            }
+        }
+    }
+}

+ 2 - 3
FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift

@@ -88,7 +88,7 @@ extension OverrideConfig {
                                     showOverrideCreationSheet = true
                                 }, label: {
                                     HStack {
-                                        Text("Add Override")
+                                        Text("New Override")
                                         Image(systemName: "plus")
                                     }
                                 })
@@ -338,7 +338,7 @@ extension OverrideConfig {
                             .font(.callout)
                             .controlSize(.mini)
 
-                        Button { isPromptPresented = true }
+                        Button { state.save() }
                         label: { Text("Save as preset") }
                             .disabled(state.durationTT == 0)
                             .tint(.orange)
@@ -495,7 +495,6 @@ extension OverrideConfig {
 
         @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
             let target = (state.units == .mgdL ? preset.target : preset.target?.decimalValue.asMmolL as NSDecimalNumber?) ?? 0
-
             let duration = (preset.duration ?? 0) as Decimal
             let name = preset.name ?? ""
             let percent = preset.percentage / 100