Mike Plante преди 1 година
родител
ревизия
2f69adf6fa
променени са 1 файла, в които са добавени 126 реда и са изтрити 26 реда
  1. 126 26
      Trio/Sources/Views/TextFieldWithToolBar.swift

+ 126 - 26
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,27 +101,104 @@ 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) { 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) {
-                if text != 0 {
-                    localText = numberFormatter.string(from: text as NSNumber) ?? ""
-                } else if localText != "" {
-                    localText = ""
+            .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
@@ -126,25 +206,32 @@ public struct TextFieldWithToolBar: View {
     }
 
     private func handleTextChange(_ oldValue: String, _ newValue: String) {
-        // Handle empty 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)
+            // 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)
@@ -152,45 +239,58 @@ 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
         }
 
-        // Check if the new value exceeds digit limits
+        // Validate against number formatter digit limits
         let components = processedText.components(separatedBy: currentDecimalSeparator)
 
-        // Count integer digits (before decimal separator)
-        let integerDigits = components[0].filter { $0.isNumber }.count
+        // 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
 
-        // Check against the number formatter limits
+        // Validate against the formatter's digit limits
         if integerDigits > numberFormatter.maximumIntegerDigits ||
             (allowDecimalSeparator && fractionDigits > numberFormatter.maximumFractionDigits)
         {
-            // Exceeds limits, don't update the text
+            // Reject input that exceeds digit limits
             localText = oldValue
             return
         }
 
-        // Update if valid decimal
+        // 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)
-            }
 
-            // 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
+            // Reject invalid decimal inputs
             localText = oldValue
         }
     }