Преглед изворни кода

Merge pull request #379 from MikePlante1/swiftUI-textfield

TextFieldWithToolBar updates
Deniz Cengiz пре 1 година
родитељ
комит
778610be6b

+ 20 - 1
Trio/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -16,6 +16,21 @@ extension Calibrations {
             return formatter
         }
 
+        private var manualGlucoseFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            if state.units == .mgdL {
+                formatter.maximumIntegerDigits = 3
+                formatter.maximumFractionDigits = 0
+            } else {
+                formatter.maximumIntegerDigits = 2
+                formatter.minimumFractionDigits = 0
+                formatter.maximumFractionDigits = 1
+            }
+            formatter.roundingMode = .halfUp
+            return formatter
+        }
+
         private var dateFormatter: DateFormatter {
             let formatter = DateFormatter()
             formatter.timeStyle = .short
@@ -30,7 +45,11 @@ extension Calibrations {
                         HStack {
                             Text("Meter glucose")
                             Spacer()
-                            TextFieldWithToolBar(text: $state.newCalibration, placeholder: "0", numberFormatter: formatter)
+                            TextFieldWithToolBar(
+                                text: $state.newCalibration,
+                                placeholder: "0",
+                                numberFormatter: manualGlucoseFormatter
+                            )
                             Text(state.units.rawValue).foregroundColor(.secondary)
                         }
                         Button {

+ 11 - 3
Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -40,6 +40,14 @@ struct CarbEntryEditorView: View {
         _editedDate = State(initialValue: Date())
     }
 
+    private var mealFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumIntegerDigits = 3
+        formatter.maximumFractionDigits = 0
+        return formatter
+    }
+
     private var carbLimitExceeded: Bool {
         editedCarbs > state.settingsManager.settings.maxCarbs
     }
@@ -130,7 +138,7 @@ struct CarbEntryEditorView: View {
                             text: $editedCarbs,
                             placeholder: "0",
                             keyboardType: .numberPad,
-                            numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                            numberFormatter: mealFormatter
                         )
                         Text("g").foregroundStyle(.secondary)
                     }
@@ -142,7 +150,7 @@ struct CarbEntryEditorView: View {
                                 text: $editedProtein,
                                 placeholder: "0",
                                 keyboardType: .numberPad,
-                                numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                                numberFormatter: mealFormatter
                             )
                             Text("g").foregroundStyle(.secondary)
                         }
@@ -153,7 +161,7 @@ struct CarbEntryEditorView: View {
                                 text: $editedFat,
                                 placeholder: "0",
                                 keyboardType: .numberPad,
-                                numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                                numberFormatter: mealFormatter
                             )
                             Text("g").foregroundStyle(.secondary)
                         }

+ 6 - 3
Trio/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -61,8 +61,11 @@ extension DataTable {
         private var manualGlucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
-            if state.units == .mmolL {
+            if state.units == .mgdL {
+                formatter.maximumIntegerDigits = 3
+                formatter.maximumFractionDigits = 0
+            } else {
+                formatter.maximumIntegerDigits = 2
                 formatter.minimumFractionDigits = 0
                 formatter.maximumFractionDigits = 1
             }
@@ -433,7 +436,7 @@ extension DataTable {
                                 TextFieldWithToolBar(
                                     text: $state.manualGlucose,
                                     placeholder: " ... ",
-                                    maxValue: limitHigh,
+                                    keyboardType: state.units == .mgdL ? .numberPad : .decimalPad,
                                     numberFormatter: manualGlucoseFormatter,
                                     initialFocus: true
                                 )

+ 1 - 0
Trio/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift

@@ -12,6 +12,7 @@ extension ManualTempBasal {
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
+            formatter.maximumIntegerDigits = 2
             formatter.maximumFractionDigits = 2
             return formatter
         }

+ 13 - 3
Trio/Sources/Modules/Treatments/View/MealPreset/AddMealPresetView.swift

@@ -18,15 +18,24 @@ struct AddMealPresetView: View {
     private var mealFormatter: NumberFormatter {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 1
+        formatter.maximumIntegerDigits = 3
+        formatter.maximumFractionDigits = 0
         return formatter
     }
 
+    private var isFormValid: Bool {
+        !dish.isEmpty && (presetCarbs > 0 || presetProtein > 0 || presetFat > 0)
+    }
+
     var body: some View {
         NavigationStack {
             Form {
                 Section {
-                    TextField("Name Of Dish", text: $dish)
+                    TextFieldWithToolBarString(
+                        text: $dish,
+                        placeholder: String(localized: "Name Of Dish"),
+                        maxLength: 25
+                    )
                 } header: {
                     Text("New Preset")
                 }
@@ -107,8 +116,9 @@ struct AddMealPresetView: View {
                 .foregroundStyle(Color.white)
                 .frame(maxWidth: .infinity, alignment: .center)
         }
-        .listRowBackground(Color(.systemBlue))
+        .listRowBackground(isFormValid ? Color(.systemBlue) : Color(.systemGray))
         .shadow(radius: 3)
         .clipShape(RoundedRectangle(cornerRadius: 8))
+        .disabled(!isFormValid)
     }
 }

+ 8 - 6
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -36,6 +36,7 @@ extension Treatments {
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
+            formatter.maximumIntegerDigits = 2
             formatter.maximumFractionDigits = 2
             return formatter
         }
@@ -43,7 +44,8 @@ extension Treatments {
         private var mealFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 1
+            formatter.maximumIntegerDigits = 3
+            formatter.maximumFractionDigits = 0
             return formatter
         }
 
@@ -51,8 +53,12 @@ extension Treatments {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             if state.units == .mmolL {
+                formatter.maximumIntegerDigits = 2
                 formatter.maximumFractionDigits = 1
-            } else { formatter.maximumFractionDigits = 0 }
+            } else {
+                formatter.maximumIntegerDigits = 3
+                formatter.maximumFractionDigits = 0
+            }
             return formatter
         }
 
@@ -84,7 +90,6 @@ extension Treatments {
                         text: $state.protein,
                         placeholder: "0",
                         keyboardType: .numberPad,
-                        maxValue: state.maxProtein,
                         numberFormatter: mealFormatter,
                         showArrows: true,
                         previousTextField: { focusedField = previousField(from: .protein) },
@@ -102,7 +107,6 @@ extension Treatments {
                         text: $state.fat,
                         placeholder: "0",
                         keyboardType: .numberPad,
-                        maxValue: state.maxFat,
                         numberFormatter: mealFormatter,
                         showArrows: true,
                         previousTextField: { focusedField = previousField(from: .fat) },
@@ -122,7 +126,6 @@ extension Treatments {
                     text: $state.carbs,
                     placeholder: "0",
                     keyboardType: .numberPad,
-                    maxValue: state.maxCarbs,
                     numberFormatter: mealFormatter,
                     showArrows: true,
                     previousTextField: { focusedField = previousField(from: .carbs) },
@@ -317,7 +320,6 @@ extension Treatments {
                                     placeholder: "0",
                                     textColor: colorScheme == .dark ? .white : .blue,
                                     maxLength: 5,
-                                    maxValue: state.maxExternal,
                                     numberFormatter: formatter,
                                     showArrows: true,
                                     previousTextField: { focusedField = previousField(from: .bolus) },

+ 146 - 23
Trio/Sources/Views/TextFieldWithToolBar.swift

@@ -21,6 +21,8 @@ public struct TextFieldWithToolBar: View {
 
     @FocusState private var isFocused: Bool
     @State private var localText: String = ""
+    // State flag to track if the field was intentionally cleared to zero
+    @State private var isZeroCleared: Bool = false
 
     public init(
         text: Binding<Decimal>,
@@ -71,6 +73,7 @@ public struct TextFieldWithToolBar: View {
                         Button(action: {
                             localText = ""
                             text = 0
+                            isZeroCleared = true // Mark as cleared to prevent showing "0"
                             textDidChange?(0)
                         }) {
                             Image(systemName: "trash")
@@ -98,46 +101,137 @@ public struct TextFieldWithToolBar: View {
             .onChange(of: isFocused) { _, newValue in
                 if newValue {
                     textFieldDidBeginEditing?()
+                    // When gaining focus: if the value is zero and was previously cleared,
+                    // keep the text field empty to show placeholder instead of "0"
+                    if isZeroCleared, text == 0 {
+                        localText = ""
+                    }
                 } else {
-                    // Format when losing focus
-                    if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
-                        text = decimal
-                        localText = numberFormatter.string(from: decimal as NSNumber) ?? ""
+                    // When losing focus: handle formatting and validation
+                    if localText.isEmpty {
+                        // If field is empty, maintain zero value but mark as cleared
+                        // so we can show placeholder instead of "0"
+                        text = 0
+                        isZeroCleared = true
+                    } else if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
+                        if decimal != 0 {
+                            // For non-zero values, format normally and update binding
+                            text = decimal
+                            localText = numberFormatter.string(from: decimal as NSNumber) ?? ""
+                            isZeroCleared = false
+                        } else {
+                            // If user explicitly entered zero, store the value but
+                            // keep display empty to show placeholder
+                            text = 0
+                            localText = ""
+                            isZeroCleared = true
+                        }
                     }
                 }
             }
-            .onChange(of: localText) { _, newValue in
-                handleTextChange(newValue)
+            .onChange(of: localText) { oldValue, newValue in
+                // Reset zero-cleared state as soon as user starts typing anything
+                if !newValue.isEmpty {
+                    isZeroCleared = false
+                }
+
+                // Special handling for backspace operations to maintain decimal format
+                if oldValue.count == newValue.count + 1 {
+                    let decimalSeparator = numberFormatter.decimalSeparator ?? "."
+
+                    // Special case: When backspacing to leave only a decimal point
+                    // e.g., "10.1" -> "10." - Keep decimal separator without adding trailing zero
+                    if newValue.hasSuffix(decimalSeparator) {
+                        if let decimal = Decimal(string: newValue + "0", locale: numberFormatter.locale) {
+                            text = decimal
+                            textDidChange?(decimal)
+                        }
+                        return
+                    }
+
+                    // Special case: When backspacing the last digit after a decimal point
+                    // e.g., "10.0" -> "10." - Ensure we keep proper decimal format
+                    if oldValue.contains(decimalSeparator), newValue.contains(decimalSeparator) {
+                        let oldParts = oldValue.components(separatedBy: decimalSeparator)
+                        let newParts = newValue.components(separatedBy: decimalSeparator)
+
+                        // Check if we've removed the last digit after decimal point
+                        if oldParts.count > 1, newParts.count > 1,
+                           oldParts[1].count == 1, newParts[1].isEmpty
+                        {
+                            // Keep proper decimal format by adding trailing zero
+                            localText = newParts[0] + decimalSeparator + "0"
+
+                            if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
+                                text = decimal
+                                textDidChange?(decimal)
+                            }
+                            return
+                        }
+                    }
+                }
+
+                // Process normal text input changes
+                handleTextChange(oldValue, newValue)
+            }
+            .onChange(of: text) { oldValue, newValue in
+                // Handle external changes to the text binding
+                // (changes not initiated by typing, like programmatic changes)
+                if oldValue != newValue,
+                   Decimal(string: localText, locale: numberFormatter.locale) != newValue
+                {
+                    if newValue == 0, isZeroCleared {
+                        // If value is zero and field was cleared, keep display empty to show placeholder
+                        localText = ""
+                    } else {
+                        // Otherwise format and display the new value
+                        localText = numberFormatter.string(from: newValue as NSNumber) ?? ""
+                        isZeroCleared = false
+                    }
+                }
             }
             .onAppear {
                 if text != 0 {
+                    // Initialize with formatted non-zero value
                     localText = numberFormatter.string(from: text as NSNumber) ?? ""
+                    isZeroCleared = false
+                } else {
+                    // For zero values, start with empty field to show placeholder
+                    localText = ""
+                    isZeroCleared = true
                 }
                 // Set initial focus if requested
                 isFocused = initialFocus
             }
     }
 
-    private func handleTextChange(_ newValue: String) {
-        // Handle empty string
+    private func handleTextChange(_ oldValue: String, _ newValue: String) {
+        // Handle empty input (clear operation)
         if newValue.isEmpty {
             text = 0
+            isZeroCleared = true
             textDidChange?(0)
             return
         }
 
+        // Remove leading zeros except for decimal values (e.g., "0.5")
+        // This prevents inputs like "01", "0123", etc. but allows "0.5"
+        if newValue.count > 1 && newValue.hasPrefix("0") && !newValue.hasPrefix("0" + (numberFormatter.decimalSeparator ?? ".")) {
+            localText = String(newValue.dropFirst())
+            return
+        }
+
         let currentDecimalSeparator = numberFormatter.decimalSeparator ?? "."
 
-        // Prevent multiple decimal separators
+        // Ensure there's only one decimal separator
         let decimalSeparatorCount = newValue.filter { String($0) == currentDecimalSeparator }.count
         if decimalSeparatorCount > 1 {
-            // If there's already a decimal separator, prevent adding another one
-            // by removing the last character (which would be the second decimal separator)
-            localText = String(newValue.dropLast())
+            // Reject input with multiple decimal separators
+            localText = oldValue
             return
         }
 
-        // Replace wrong decimal separator with the correct one
+        // Handle localization by converting to the correct decimal separator
         var processedText = newValue
         if newValue.contains("."), currentDecimalSeparator != "." {
             processedText = newValue.replacingOccurrences(of: ".", with: currentDecimalSeparator)
@@ -145,30 +239,59 @@ public struct TextFieldWithToolBar: View {
             processedText = newValue.replacingOccurrences(of: ",", with: currentDecimalSeparator)
         }
 
-        // Handle leading decimal separator
+        // Automatically add leading zero when starting with decimal separator
+        // For example ".5" becomes "0.5"
         if processedText.hasPrefix(currentDecimalSeparator) {
             processedText = "0" + processedText
         }
 
-        // Update if valid decimal
+        // Validate against number formatter digit limits
+        let components = processedText.components(separatedBy: currentDecimalSeparator)
+
+        // Process the integer part (before decimal)
+        var integerPart = components[0].filter { $0.isNumber }
+        // Remove leading zeros for accurate digit counting
+        while integerPart.hasPrefix("0") && integerPart.count > 1 {
+            integerPart.removeFirst()
+        }
+        let integerDigits = integerPart.count
+
+        // Count fraction digits (after decimal separator)
+        let fractionDigits = components.count > 1 ? components[1].filter { $0.isNumber }.count : 0
+
+        // Validate against the formatter's digit limits
+        if integerDigits > numberFormatter.maximumIntegerDigits ||
+            (allowDecimalSeparator && fractionDigits > numberFormatter.maximumFractionDigits)
+        {
+            // Reject input that exceeds digit limits
+            localText = oldValue
+            return
+        }
+
+        // Parse and validate the decimal value
         if let decimal = Decimal(string: processedText, locale: numberFormatter.locale) {
             if let maxValue = maxValue, decimal > maxValue {
+                // Cap at maximum allowed value
                 text = maxValue
                 localText = numberFormatter.string(from: maxValue as NSNumber) ?? ""
+                isZeroCleared = false
             } else {
+                // Accept valid input and update binding
                 text = decimal
+
+                // Update zero-cleared state based on the value
+                isZeroCleared = (decimal == 0) && localText.isEmpty
+
                 textDidChange?(decimal)
-            }
-//            text = decimal
-//            textDidChange?(decimal)
 
-            // If the processed text is different from the input, update the field
-            if processedText != newValue {
-                localText = processedText
+                // If we had to process/modify the input, update the displayed text
+                if processedText != newValue {
+                    localText = processedText
+                }
             }
         } else {
-            // If not a valid decimal, keep the old value
-            localText = numberFormatter.string(from: text as NSNumber) ?? ""
+            // Reject invalid decimal inputs
+            localText = oldValue
         }
     }
 }