Pārlūkot izejas kodu

Refactor guardrail alert

Jonas Björkert 1 gadu atpakaļ
vecāks
revīzija
fd5329b014

+ 4 - 4
LoopFollow.xcodeproj/project.pbxproj

@@ -30,7 +30,7 @@
 		DD48780A2C7B30D40048F05C /* RemoteSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878092C7B30D40048F05C /* RemoteSettingsViewModel.swift */; };
 		DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48780D2C7B74A40048F05C /* TrioRemoteControlViewModel.swift */; };
 		DD4878102C7B74BF0048F05C /* TrioRemoteControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48780F2C7B74BF0048F05C /* TrioRemoteControlView.swift */; };
-		DD4878132C7B750D0048F05C /* TempTargetView.swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878122C7B750D0048F05C /* TempTargetView.swift.swift */; };
+		DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878122C7B750D0048F05C /* TempTargetView.swift */; };
 		DD4878152C7B75230048F05C /* MealView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878142C7B75230048F05C /* MealView.swift */; };
 		DD4878172C7B75350048F05C /* BolusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878162C7B75350048F05C /* BolusView.swift */; };
 		DD4878192C7C56D60048F05C /* TrioNightscoutRemoteController.swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878182C7C56D60048F05C /* TrioNightscoutRemoteController.swift.swift */; };
@@ -267,7 +267,7 @@
 		DD4878092C7B30D40048F05C /* RemoteSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSettingsViewModel.swift; sourceTree = "<group>"; };
 		DD48780D2C7B74A40048F05C /* TrioRemoteControlViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControlViewModel.swift; sourceTree = "<group>"; };
 		DD48780F2C7B74BF0048F05C /* TrioRemoteControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControlView.swift; sourceTree = "<group>"; };
-		DD4878122C7B750D0048F05C /* TempTargetView.swift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetView.swift.swift; sourceTree = "<group>"; };
+		DD4878122C7B750D0048F05C /* TempTargetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetView.swift; sourceTree = "<group>"; };
 		DD4878142C7B75230048F05C /* MealView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealView.swift; sourceTree = "<group>"; };
 		DD4878162C7B75350048F05C /* BolusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusView.swift; sourceTree = "<group>"; };
 		DD4878182C7C56D60048F05C /* TrioNightscoutRemoteController.swift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioNightscoutRemoteController.swift.swift; sourceTree = "<group>"; };
@@ -569,7 +569,7 @@
 		DD4878112C7B74F90048F05C /* TRC */ = {
 			isa = PBXGroup;
 			children = (
-				DD4878122C7B750D0048F05C /* TempTargetView.swift.swift */,
+				DD4878122C7B750D0048F05C /* TempTargetView.swift */,
 				DD4878142C7B75230048F05C /* MealView.swift */,
 				DD4878162C7B75350048F05C /* BolusView.swift */,
 			);
@@ -1268,7 +1268,7 @@
 				DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */,
 				DDF699962C5582290058A8D9 /* TextFieldWithToolBar.swift in Sources */,
 				DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */,
-				DD4878132C7B750D0048F05C /* TempTargetView.swift.swift in Sources */,
+				DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */,
 				DD0C0C682C48529400DBADDF /* Metric.swift in Sources */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,
 				FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */,

+ 10 - 78
LoopFollow/Helpers/TextFieldWithToolBar.swift

@@ -13,9 +13,6 @@ import HealthKit
 public struct TextFieldWithToolBar: UIViewRepresentable {
     @Binding var quantity: HKQuantity
 
-    @State private var alertMessage: String? = nil
-    @State private var showAlert: Bool = false
-
     var textColor: UIColor
     var textAlignment: NSTextAlignment
     var autocapitalizationType: UITextAutocapitalizationType
@@ -28,6 +25,7 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
     var allowDecimalSeparator: Bool
     var minValue: HKQuantity?
     var maxValue: HKQuantity?
+    var onValidationError: (String) -> Void
 
     public init(
         quantity: Binding<HKQuantity>,
@@ -42,7 +40,8 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         unit: HKUnit,
         allowDecimalSeparator: Bool = true,
         minValue: HKQuantity? = nil,
-        maxValue: HKQuantity? = nil
+        maxValue: HKQuantity? = nil,
+        onValidationError: @escaping (String) -> Void
     ) {
         _quantity = quantity
         self.textColor = textColor
@@ -57,6 +56,7 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         self.allowDecimalSeparator = allowDecimalSeparator
         self.minValue = minValue
         self.maxValue = maxValue
+        self.onValidationError = onValidationError
     }
 
     private func formattedPlaceholder(for unit: HKUnit) -> String {
@@ -77,15 +77,6 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         textField.text = quantity.doubleValue(for: unit) == 0 ? "" : context.coordinator.format(quantity: quantity, for: unit)
         textField.placeholder = formattedPlaceholder(for: unit)
         textField.keyboardType = unit.preferredFractionDigits == 0 ? .numberPad : .decimalPad
-
-        if showAlert {
-            let alert = UIAlertController(title: "Input Error", message: alertMessage, preferredStyle: .alert)
-            alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
-                showAlert = false
-            })
-            UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil)
-        }
-
         return textField
     }
 
@@ -104,7 +95,6 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
             target: context.coordinator,
             action: #selector(Coordinator.clearText)
         )
-
         toolbar.items = [clearButton, flexibleSpace, doneButton]
         toolbar.sizeToFit()
         return toolbar
@@ -134,7 +124,7 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
     }
 
     public func makeCoordinator() -> Coordinator {
-        Coordinator(self, maxLength: maxLength, unit: unit, minValue: minValue, maxValue: maxValue)
+        Coordinator(self, maxLength: maxLength, unit: unit, minValue: minValue, maxValue: maxValue, onValidationError: onValidationError)
     }
 
     public final class Coordinator: NSObject, UITextFieldDelegate {
@@ -146,13 +136,15 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         var unit: HKUnit
         var minValue: HKQuantity?
         var maxValue: HKQuantity?
+        let onValidationError: (String) -> Void
 
-        init(_ parent: TextFieldWithToolBar, maxLength: Int?, unit: HKUnit, minValue: HKQuantity?, maxValue: HKQuantity?) {
+        init(_ parent: TextFieldWithToolBar, maxLength: Int?, unit: HKUnit, minValue: HKQuantity?, maxValue: HKQuantity?, onValidationError: @escaping (String) -> Void) {
             self.parent = parent
             self.maxLength = maxLength
             self.unit = unit
             self.minValue = minValue
             self.maxValue = maxValue
+            self.onValidationError = onValidationError
         }
 
         @objc fileprivate func clearText() {
@@ -192,72 +184,12 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
                         if let maxValue = self.parent.maxValue {
                             message += "Maximum: \(self.format(quantity: maxValue, for: self.unit))"
                         }
-                        self.showInvalidInputAlert(textField, message: message)
+                        self.onValidationError(message)
                     }
                 } else {
-                    self.showInvalidInputAlert(textField, message: "Invalid number format")
-                }
-            }
-        }
-
-        private func showInvalidInputAlert(_ textField: UITextField, message: String) {
-            var message = "Value outside of guardrails\n"
-            let preferredFractionDigits = self.unit.preferredFractionDigits
-            let roundingFactor = pow(10.0, Double(preferredFractionDigits))
-
-            if let minValue = self.parent.minValue {
-                let minValueValue = minValue.doubleValue(for: self.unit)
-                let minValueRoundedUp = ceil(minValueValue * roundingFactor) / roundingFactor
-                message += "Minimum: \(self.format(quantity: HKQuantity(unit: self.unit, doubleValue: minValueRoundedUp), for: self.unit))\n"
-            }
-            if let maxValue = self.parent.maxValue {
-                let maxValueValue = maxValue.doubleValue(for: self.unit)
-                let maxValueRoundedDown = floor(maxValueValue * roundingFactor) / roundingFactor
-                message += "Maximum: \(self.format(quantity: HKQuantity(unit: self.unit, doubleValue: maxValueRoundedDown), for: self.unit))"
-            }
-            let alert = UIAlertController(title: "Input Validation Error", message: message, preferredStyle: .alert)
-            alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
-                textField.becomeFirstResponder()
-            })
-            alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
-            })
-            UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil)
-        }
-
-        public func textField(
-            _ textField: UITextField,
-            shouldChangeCharactersIn range: NSRange,
-            replacementString string: String
-        ) -> Bool {
-            let decimalSeparator = Locale.current.decimalSeparator ?? "."
-            let isNumber = CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string))
-            let isDecimalSeparator = (string == decimalSeparator && textField.text?.contains(decimalSeparator) == false)
-
-            let currentText = textField.text ?? ""
-            let proposedText = (currentText as NSString).replacingCharacters(in: range, with: string)
-
-            if let maxLength = maxLength, proposedText.count > maxLength {
-                return false
-            }
-
-            let isValidInput = isNumber || (isDecimalSeparator && parent.allowDecimalSeparator && unit.preferredFractionDigits > 0)
-
-            if isValidInput, let number = Double(proposedText.replacingOccurrences(of: decimalSeparator, with: ".")) {
-                let quantity = HKQuantity(unit: unit, doubleValue: number)
-                if isWithinLimits(quantity) {
-                    parent.quantity = quantity
-                } else {
-                    parent.quantity = HKQuantity(unit: unit, doubleValue: 0)
+                    self.onValidationError("Invalid number format")
                 }
-            } else {
-                parent.quantity = HKQuantity(unit: unit, doubleValue: 0)
             }
-
-            return isValidInput
-        }
-
-        public func textFieldDidBeginEditing(_: UITextField) {
-            parent.textFieldDidBeginEditing?()
         }
 
         func format(quantity: HKQuantity, for unit: HKUnit) -> String {

+ 4 - 1
LoopFollow/Helpers/Views/HKQuantityInputView.swift

@@ -19,6 +19,8 @@ struct HKQuantityInputView: View {
     var maxValue: HKQuantity
     @FocusState.Binding var isFocused: Bool
 
+    var onValidationError: (String) -> Void
+
     var body: some View {
         HStack {
             Text(label)
@@ -28,7 +30,8 @@ struct HKQuantityInputView: View {
                 maxLength: maxLength,
                 unit: unit,
                 minValue: minValue,
-                maxValue: maxValue
+                maxValue: maxValue,
+                onValidationError: onValidationError
             )
             .focused($isFocused)
             Text(unit.localizedShortUnitString)

+ 45 - 7
LoopFollow/Remote/Settings/RemoteSettingsView.swift

@@ -15,6 +15,10 @@ struct RemoteSettingsView: View {
     @Environment(\.presentationMode) var presentationMode
     @FocusState private var focusedField: Field?
 
+    @State private var showAlert: Bool = false
+    @State private var alertType: AlertType? = nil
+    @State private var alertMessage: String? = nil
+
     enum Field: Hashable {
         case user
         case deviceToken
@@ -26,6 +30,10 @@ struct RemoteSettingsView: View {
         case maxBolus
     }
 
+    enum AlertType {
+        case validation
+    }
+
     var body: some View {
         NavigationView {
             Form {
@@ -63,7 +71,7 @@ struct RemoteSettingsView: View {
                                 .disableAutocorrection(true)
                                 .focused($focusedField, equals: .deviceToken)
                                 .multilineTextAlignment(.trailing)
-                                .disabled(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
+                                .disabled(true)
                         }
 
                         HStack {
@@ -77,7 +85,7 @@ struct RemoteSettingsView: View {
 
                         Toggle("Production Environment", isOn: $viewModel.productionEnvironment)
                             .padding(.vertical, 5)
-                            .disabled(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
+                            .disabled(true)
 
                         VStack(alignment: .leading) {
                             Text("Token")
@@ -118,7 +126,7 @@ struct RemoteSettingsView: View {
                                 .focused($focusedField, equals: .bundleId)
                                 .multilineTextAlignment(.trailing)
                         }
-                        .disabled(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
+                        .disabled(true)
                     }
 
                     // Guardrails Section
@@ -132,7 +140,10 @@ struct RemoteSettingsView: View {
                                 unit: HKUnit.internationalUnit(),
                                 allowDecimalSeparator: true,
                                 minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.0),
-                                maxValue: HKQuantity(unit: .internationalUnit(), doubleValue: 10.0)
+                                maxValue: HKQuantity(unit: .internationalUnit(), doubleValue: 10.0),
+                                onValidationError: { message in
+                                    handleValidationError(message)
+                                }
                             )
                             .frame(width: 100)
                             Text("U")
@@ -148,7 +159,10 @@ struct RemoteSettingsView: View {
                                 unit: HKUnit.gram(),
                                 allowDecimalSeparator: true,
                                 minValue: HKQuantity(unit: .gram(), doubleValue: 0),
-                                maxValue: HKQuantity(unit: .gram(), doubleValue: 100)
+                                maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
+                                onValidationError: { message in
+                                    handleValidationError(message)
+                                }
                             )
                             .frame(width: 100)
                             Text("g")
@@ -164,7 +178,10 @@ struct RemoteSettingsView: View {
                                 unit: HKUnit.gram(),
                                 allowDecimalSeparator: true,
                                 minValue: HKQuantity(unit: .gram(), doubleValue: 0),
-                                maxValue: HKQuantity(unit: .gram(), doubleValue: 100)
+                                maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
+                                onValidationError: { message in
+                                    handleValidationError(message)
+                                }
                             )
                             .frame(width: 100)
                             Text("g")
@@ -180,7 +197,10 @@ struct RemoteSettingsView: View {
                                 unit: HKUnit.gram(),
                                 allowDecimalSeparator: true,
                                 minValue: HKQuantity(unit: .gram(), doubleValue: 0),
-                                maxValue: HKQuantity(unit: .gram(), doubleValue: 100)
+                                maxValue: HKQuantity(unit: .gram(), doubleValue: 100),
+                                onValidationError: { message in
+                                    handleValidationError(message)
+                                }
                             )
                             .frame(width: 100)
                             Text("g")
@@ -200,6 +220,24 @@ struct RemoteSettingsView: View {
             .onTapGesture {
                 focusedField = nil
             }
+            .alert(isPresented: $showAlert) {
+                switch alertType {
+                case .validation:
+                    return Alert(
+                        title: Text("Validation Error"),
+                        message: Text(alertMessage ?? "Invalid input."),
+                        dismissButton: .default(Text("OK"))
+                    )
+                case .none:
+                    return Alert(title: Text("Unknown Alert"))
+                }
+            }
         }
     }
+
+    private func handleValidationError(_ message: String) {
+        alertMessage = message
+        alertType = .validation
+        showAlert = true
+    }
 }

+ 19 - 2
LoopFollow/Remote/TRC/BolusView.swift

@@ -19,12 +19,14 @@ struct BolusView: View {
 
     @State private var showAlert = false
     @State private var alertType: AlertType? = nil
+    @State private var alertMessage: String? = nil
     @State private var isLoading = false
     @State private var statusMessage: String? = nil
 
     enum AlertType {
         case confirmBolus
         case status
+        case validation
     }
 
     var body: some View {
@@ -39,7 +41,10 @@ struct BolusView: View {
                             maxLength: 4,
                             minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0.05),
                             maxValue: maxBolus.value,
-                            isFocused: $bolusFieldIsFocused
+                            isFocused: $bolusFieldIsFocused,
+                            onValidationError: { message in
+                                handleValidationError(message)
+                            }
                         )
                     }
 
@@ -49,7 +54,7 @@ struct BolusView: View {
                         isLoading: isLoading,
                         action: {
                             bolusFieldIsFocused = false
-                            if bolusAmount.doubleValue(for: HKUnit.internationalUnit()) != 0.0 {
+                            if bolusAmount.doubleValue(for: HKUnit.internationalUnit()) > 0.0 {
                                 alertType = .confirmBolus
                                 showAlert = true
                             }
@@ -85,6 +90,12 @@ struct BolusView: View {
                         message: Text(statusMessage ?? ""),
                         dismissButton: .default(Text("OK"))
                     )
+                case .validation:
+                    return Alert(
+                        title: Text("Validation Error"),
+                        message: Text(alertMessage ?? "Invalid input."),
+                        dismissButton: .default(Text("OK"))
+                    )
                 case .none:
                     return Alert(title: Text("Unknown Alert"))
                 }
@@ -134,4 +145,10 @@ struct BolusView: View {
             }
         }
     }
+
+    private func handleValidationError(_ message: String) {
+        alertMessage = message
+        alertType = .validation
+        showAlert = true
+    }
 }

+ 29 - 3
LoopFollow/Remote/TRC/MealView.swift

@@ -14,6 +14,7 @@ struct MealView: View {
     @State private var protein = HKQuantity(unit: .gram(), doubleValue: 0.0)
     @State private var fat = HKQuantity(unit: .gram(), doubleValue: 0.0)
     private let pushNotificationManager = PushNotificationManager()
+
     @ObservedObject private var maxCarbs = Storage.shared.maxCarbs
     @ObservedObject private var maxProtein = Storage.shared.maxProtein
     @ObservedObject private var maxFat = Storage.shared.maxFat
@@ -24,12 +25,14 @@ struct MealView: View {
 
     @State private var showAlert: Bool = false
     @State private var alertType: AlertType? = nil
+    @State private var alertMessage: String? = nil
     @State private var isLoading: Bool = false
     @State private var statusMessage: String? = nil
 
     enum AlertType {
         case confirmMeal
         case status
+        case validationError
     }
 
     var body: some View {
@@ -44,7 +47,10 @@ struct MealView: View {
                             maxLength: 4,
                             minValue: HKQuantity(unit: .gram(), doubleValue: 0),
                             maxValue: maxCarbs.value,
-                            isFocused: $carbsFieldIsFocused
+                            isFocused: $carbsFieldIsFocused,
+                            onValidationError: { message in
+                                handleValidationError(message)
+                            }
                         )
 
                         HKQuantityInputView(
@@ -54,7 +60,10 @@ struct MealView: View {
                             maxLength: 4,
                             minValue: HKQuantity(unit: .gram(), doubleValue: 0),
                             maxValue: maxProtein.value,
-                            isFocused: $proteinFieldIsFocused
+                            isFocused: $proteinFieldIsFocused,
+                            onValidationError: { message in
+                                handleValidationError(message)
+                            }
                         )
 
                         HKQuantityInputView(
@@ -64,7 +73,10 @@ struct MealView: View {
                             maxLength: 4,
                             minValue: HKQuantity(unit: .gram(), doubleValue: 0),
                             maxValue: maxFat.value,
-                            isFocused: $fatFieldIsFocused
+                            isFocused: $fatFieldIsFocused,
+                            onValidationError: { message in
+                                handleValidationError(message)
+                            }
                         )
                     }
 
@@ -129,6 +141,14 @@ struct MealView: View {
                             showAlert = false
                         })
                     )
+                case .validationError:
+                    return Alert(
+                        title: Text("Validation Error"),
+                        message: Text(alertMessage ?? ""),
+                        dismissButton: .default(Text("OK"), action: {
+                            showAlert = false
+                        })
+                    )
                 case .none:
                     return Alert(title: Text("Unknown Alert"))
                 }
@@ -159,4 +179,10 @@ struct MealView: View {
             }
         }
     }
+
+    private func handleValidationError(_ message: String) {
+        alertMessage = message
+        alertType = .validationError
+        showAlert = true
+    }
 }

+ 24 - 6
LoopFollow/Remote/TRC/TempTargetView.swift.swift

@@ -1,5 +1,5 @@
 //
-//  TempTargetView.swift.swift
+//  TempTargetView.swift
 //  LoopFollow
 //
 //  Created by Jonas Björkert on 2024-08-25.
@@ -19,6 +19,7 @@ struct TempTargetView: View {
     @State private var duration = HKQuantity(unit: .minute(), doubleValue: 0.0)
     @State private var showAlert: Bool = false
     @State private var alertType: AlertType? = nil
+    @State private var alertMessage: String? = nil
     @State private var isLoading: Bool = false
     @State private var statusMessage: String? = nil
 
@@ -32,6 +33,7 @@ struct TempTargetView: View {
     enum AlertType {
         case confirmCommand
         case status
+        case validation
         case confirmCancellation
     }
 
@@ -75,7 +77,10 @@ struct TempTargetView: View {
                                     maxLength: 4,
                                     unit: UserDefaultsRepository.getPreferredUnit(),
                                     minValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 60),
-                                    maxValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 300)
+                                    maxValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 300),
+                                    onValidationError: { message in
+                                        handleValidationError(message)
+                                    }
                                 )
                                 .focused($targetFieldIsFocused)
                                 Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
@@ -87,7 +92,10 @@ struct TempTargetView: View {
                                     quantity: $duration,
                                     maxLength: 4,
                                     unit: HKUnit.minute(),
-                                    minValue: HKQuantity(unit: .minute(), doubleValue: 5)
+                                    minValue: HKQuantity(unit: .minute(), doubleValue: 5),
+                                    onValidationError: { message in
+                                        handleValidationError(message)
+                                    }
                                 )
                                 .focused($durationFieldIsFocused)
                                 Text("minutes").foregroundColor(.secondary)
@@ -177,9 +185,7 @@ struct TempTargetView: View {
                     return Alert(
                         title: Text("Status"),
                         message: Text(statusMessage ?? ""),
-                        dismissButton: .default(Text("OK"), action: {
-                            showAlert = false
-                        })
+                        dismissButton: .default(Text("OK"))
                     )
                 case .confirmCancellation:
                     return Alert(
@@ -190,6 +196,12 @@ struct TempTargetView: View {
                         }),
                         secondaryButton: .cancel()
                     )
+                case .validation:
+                    return Alert(
+                        title: Text("Validation Error"),
+                        message: Text(alertMessage ?? "Invalid input."),
+                        dismissButton: .default(Text("OK"))
+                    )
                 case .none:
                     return Alert(title: Text("Unknown Alert"))
                 }
@@ -253,4 +265,10 @@ struct TempTargetView: View {
             }
         }
     }
+
+    private func handleValidationError(_ message: String) {
+        alertMessage = message
+        alertType = .validation
+        showAlert = true
+    }
 }

+ 23 - 3
LoopFollow/Remote/TrioNightscoutRemoteView.swift

@@ -1,5 +1,5 @@
 //
-//  RemoteView.swift
+//  TrioNightscoutRemoteView.swift
 //  LoopFollow
 //
 //  Created by Jonas Björkert on 2024-07-19.
@@ -21,6 +21,7 @@ struct TrioNightscoutRemoteView: View {
     @State private var duration = HKQuantity(unit: .minute(), doubleValue: 0.0)
     @State private var showAlert: Bool = false
     @State private var alertType: AlertType? = nil
+    @State private var alertMessage: String? = nil
     @State private var isLoading: Bool = false
     @State private var statusMessage: String? = nil
 
@@ -34,6 +35,7 @@ struct TrioNightscoutRemoteView: View {
     enum AlertType {
         case confirmCommand
         case status
+        case validation
         case confirmCancellation
     }
 
@@ -85,7 +87,10 @@ struct TrioNightscoutRemoteView: View {
                                     maxLength: 4,
                                     unit: UserDefaultsRepository.getPreferredUnit(),
                                     minValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 60),
-                                    maxValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 300)
+                                    maxValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 300),
+                                    onValidationError: { message in
+                                        handleValidationError(message)
+                                    }
                                 )
                                 .focused($targetFieldIsFocused)
                                 Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary)
@@ -97,7 +102,10 @@ struct TrioNightscoutRemoteView: View {
                                     quantity: $duration,
                                     maxLength: 4,
                                     unit: HKUnit.minute(),
-                                    minValue: HKQuantity(unit: .minute(), doubleValue: 5)
+                                    minValue: HKQuantity(unit: .minute(), doubleValue: 5),
+                                    onValidationError: { message in
+                                        handleValidationError(message)
+                                    }
                                 )
                                 .focused($durationFieldIsFocused)
                                 Text("minutes").foregroundColor(.secondary)
@@ -200,6 +208,12 @@ struct TrioNightscoutRemoteView: View {
                         }),
                         secondaryButton: .cancel()
                     )
+                case .validation:
+                    return Alert(
+                        title: Text("Validation Error"),
+                        message: Text(alertMessage ?? "Invalid input."),
+                        dismissButton: .default(Text("OK"))
+                    )
                 case .none:
                     return Alert(title: Text("Unknown Alert"))
                 }
@@ -265,4 +279,10 @@ struct TrioNightscoutRemoteView: View {
             self.showAlert = true
         }
     }
+
+    private func handleValidationError(_ message: String) {
+        alertMessage = message
+        alertType = .validation
+        showAlert = true
+    }
 }