Bläddra i källkod

Merge branch 'dev' of github.com:nightscout/Trio-dev into dexcom-troubleshooting

Deniz Cengiz 1 år sedan
förälder
incheckning
8b08bf14ac

+ 6 - 1
Trio/Resources/Info.plist

@@ -107,7 +107,12 @@
 	<key>UIFileSharingEnabled</key>
 	<true/>
 	<key>UILaunchScreen</key>
-	<dict/>
+	<dict>
+		<key>UIColorName</key>
+		<string>Background_DarkBlue</string>
+		<key>UIImageName</key>
+		<string>trioCircledNoBackground</string>
+	</dict>
 	<key>UIRequiredDeviceCapabilities</key>
 	<array>
 		<string>armv7</string>

+ 8 - 2
Trio/Sources/Application/TrioApp.swift

@@ -145,14 +145,20 @@ extension Notification.Name {
                 Main.LoadingView(showError: $showLoadingError, retry: retryCoreDataInitialization)
                     .onAppear {
                         if self.initState.complete {
-                            self.showLoadingView = false
+                            Task { @MainActor in
+                                try? await Task.sleep(for: .seconds(1.8))
+                                self.showLoadingView = false
+                            }
                         }
                         if self.initState.error {
                             self.showLoadingError = true
                         }
                     }
                     .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationCompleted)) { _ in
-                        self.showLoadingView = false
+                        Task { @MainActor in
+                            try? await Task.sleep(for: .seconds(1.8))
+                            self.showLoadingView = false
+                        }
                     }
                     .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationError)) { _ in
                         self.showLoadingError = true

+ 1 - 1
Trio/Sources/Helpers/CustomProgressView.swift

@@ -32,7 +32,7 @@ struct CustomProgressView: View {
                         .frame(width: 80, height: 3)
                         .offset(x: self.animate ? 180 : -180, y: 0)
                         .animation(
-                            Animation.linear(duration: 2)
+                            Animation.linear(duration: 1)
                                 .repeatForever(autoreverses: false), value: UUID()
                         )
                 )

+ 0 - 4
Trio/Sources/Modules/Base/BaseStateModel.swift

@@ -11,10 +11,6 @@ protocol StateModel: ObservableObject {
     func view(for screen: Screen) -> AnyView
 }
 
-protocol CGMStateModel: StateModel {
-    var cgmCurrent: CGMType { get }
-}
-
 class BaseStateModel<Provider>: StateModel, Injectable where Provider: Trio.Provider {
     @Injected() var router: Router!
     @Injected() var settingsManager: SettingsManager!

+ 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)
                         }

+ 10 - 6
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
             }
@@ -421,8 +424,8 @@ extension DataTable {
         }
 
         @ViewBuilder private func addGlucoseView() -> some View {
-            let limitLow: Decimal = state.units == .mmolL ? 0.8 : 14
-            let limitHigh: Decimal = state.units == .mmolL ? 40 : 720
+            let limitLow: Decimal = state.units == .mgdL ? Decimal(14) : 14.asMmolL
+            let limitHigh: Decimal = state.units == .mgdL ? Decimal(720) : 720.asMmolL
 
             NavigationView {
                 VStack {
@@ -433,8 +436,9 @@ extension DataTable {
                                 TextFieldWithToolBar(
                                     text: $state.manualGlucose,
                                     placeholder: " ... ",
-                                    shouldBecomeFirstResponder: true,
-                                    numberFormatter: manualGlucoseFormatter
+                                    keyboardType: state.units == .mgdL ? .numberPad : .decimalPad,
+                                    numberFormatter: manualGlucoseFormatter,
+                                    initialFocus: true
                                 )
                                 Text(state.units.rawValue).foregroundStyle(.secondary)
                             }

+ 3 - 2
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
         }
@@ -25,8 +26,8 @@ extension ManualTempBasal {
                         TextFieldWithToolBar(
                             text: $state.rate,
                             placeholder: "0",
-                            shouldBecomeFirstResponder: true,
-                            numberFormatter: formatter
+                            numberFormatter: formatter,
+                            initialFocus: true
                         )
                         Text("U/hr").foregroundColor(.secondary)
                     }

+ 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 - 2
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
         }
 

+ 227 - 221
Trio/Sources/Views/TextFieldWithToolBar.swift

@@ -1,292 +1,298 @@
 import SwiftUI
 import UIKit
 
-public struct TextFieldWithToolBar: UIViewRepresentable {
+public struct TextFieldWithToolBar: View {
     @Binding var text: Decimal
     var placeholder: String
-    var textColor: UIColor
-    var textAlignment: NSTextAlignment
+    var textColor: Color
+    var textAlignment: TextAlignment
     var keyboardType: UIKeyboardType
-    var autocapitalizationType: UITextAutocapitalizationType
-    var autocorrectionType: UITextAutocorrectionType
-    var shouldBecomeFirstResponder: Bool
     var maxLength: Int?
+    var maxValue: Decimal?
     var isDismissible: Bool
     var textFieldDidBeginEditing: (() -> Void)?
+    var textDidChange: ((Decimal) -> Void)?
     var numberFormatter: NumberFormatter
     var allowDecimalSeparator: Bool
     var showArrows: Bool
     var previousTextField: (() -> Void)?
     var nextTextField: (() -> Void)?
+    var initialFocus: Bool
+
+    @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>,
         placeholder: String,
-        textColor: UIColor = .label,
-        textAlignment: NSTextAlignment = .right,
+        textColor: Color = .primary,
+        textAlignment: TextAlignment = .trailing,
         keyboardType: UIKeyboardType = .decimalPad,
-        autocapitalizationType: UITextAutocapitalizationType = .none,
-        autocorrectionType: UITextAutocorrectionType = .no,
-        shouldBecomeFirstResponder: Bool = false,
         maxLength: Int? = nil,
+        maxValue: Decimal? = nil,
         isDismissible: Bool = true,
         textFieldDidBeginEditing: (() -> Void)? = nil,
+        textDidChange: ((Decimal) -> Void)? = nil,
         numberFormatter: NumberFormatter,
         allowDecimalSeparator: Bool = true,
         showArrows: Bool = false,
         previousTextField: (() -> Void)? = nil,
-        nextTextField: (() -> Void)? = nil
+        nextTextField: (() -> Void)? = nil,
+        initialFocus: Bool = false
     ) {
         _text = text
         self.placeholder = placeholder
         self.textColor = textColor
         self.textAlignment = textAlignment
         self.keyboardType = keyboardType
-        self.autocapitalizationType = autocapitalizationType
-        self.autocorrectionType = autocorrectionType
-        self.shouldBecomeFirstResponder = shouldBecomeFirstResponder
         self.maxLength = maxLength
+        self.maxValue = maxValue
         self.isDismissible = isDismissible
         self.textFieldDidBeginEditing = textFieldDidBeginEditing
+        self.textDidChange = textDidChange
         self.numberFormatter = numberFormatter
         self.numberFormatter.numberStyle = .decimal
         self.allowDecimalSeparator = allowDecimalSeparator
         self.showArrows = showArrows
         self.previousTextField = previousTextField
         self.nextTextField = nextTextField
+        self.initialFocus = initialFocus
     }
 
-    public func makeUIView(context: Context) -> UITextField {
-        let textField = UITextField()
-        context.coordinator.textField = textField
-        textField.inputAccessoryView = isDismissible ? createToolbar(for: textField, context: context) : nil
-        textField.addTarget(context.coordinator, action: #selector(Coordinator.editingDidBegin), for: .editingDidBegin)
-        textField.delegate = context.coordinator
-        if text == 0 { /// show no value initially, i.e. empty String
-            textField.text = ""
-        } else {
-            textField.text = numberFormatter.string(for: text)
-        }
-        textField.placeholder = placeholder
-        return textField
-    }
-
-    /// Creates and configures a toolbar for the text field with navigation and action buttons.
-    /// - Parameters:
-    ///   - _: The text field for which the toolbar is being created (unused parameter).
-    ///   - context: The SwiftUI context that contains the coordinator for handling button actions.
-    /// - Returns: A configured UIToolbar with appropriate buttons based on the view's configuration.
-    private func createToolbar(for _: UITextField, context: Context) -> UIToolbar {
-        let toolbar = UIToolbar()
-        var items: [UIBarButtonItem] = []
-
-        // Add navigation arrows if enabled
-        if showArrows {
-            // Add clear button
-            items.append(
-                UIBarButtonItem(
-                    image: UIImage(systemName: "trash"),
-                    style: .plain,
-                    target: context.coordinator,
-                    action: #selector(Coordinator.clearText)
-                )
-            )
-
-            if previousTextField != nil {
-                let previousButton = UIBarButtonItem(
-                    image: UIImage(systemName: "chevron.up"),
-                    style: .plain,
-                    target: context.coordinator,
-                    action: #selector(Coordinator.previousTextField)
-                )
-                items.append(previousButton)
+    public var body: some View {
+        TextField(placeholder, text: $localText)
+            .focused($isFocused)
+            .multilineTextAlignment(textAlignment)
+            .foregroundColor(textColor)
+            .keyboardType(keyboardType)
+            .toolbar {
+                if isFocused {
+                    ToolbarItemGroup(placement: .keyboard) {
+                        Button(action: {
+                            localText = ""
+                            text = 0
+                            isZeroCleared = true // Mark as cleared to prevent showing "0"
+                            textDidChange?(0)
+                        }) {
+                            Image(systemName: "trash")
+                        }
+
+                        if showArrows {
+                            Button(action: { previousTextField?() }) {
+                                Image(systemName: "chevron.up")
+                            }
+                            Button(action: { nextTextField?() }) {
+                                Image(systemName: "chevron.down")
+                            }
+                        }
+
+                        Spacer()
+
+                        if isDismissible {
+                            Button(action: { isFocused = false }) {
+                                Image(systemName: "keyboard.chevron.compact.down")
+                            }
+                        }
+                    }
+                }
             }
-
-            if nextTextField != nil {
-                let nextButton = UIBarButtonItem(
-                    image: UIImage(systemName: "chevron.down"),
-                    style: .plain,
-                    target: context.coordinator,
-                    action: #selector(Coordinator.nextTextField)
-                )
-                items.append(nextButton)
+            .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 {
+                    // 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
+                        }
+                    }
+                }
             }
-        }
-
-        // Add flexible space
-        items.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil))
-
-        // Add done button
-        items.append(
-            UIBarButtonItem(
-                barButtonSystemItem: .done,
-                target: UIApplication.shared,
-                action: #selector(UIApplication.endEditing)
-            )
-        )
-
-        toolbar.items = items
-        toolbar.sizeToFit()
-
-        return toolbar
-    }
-
-    public func updateUIView(_ textField: UITextField, context: Context) {
-        if text != 0 {
-            let newText = numberFormatter.string(for: text) ?? ""
-            if textField.text != newText {
-                textField.text = newText
+            .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)
             }
-        }
-
-        textField.textColor = textColor
-        textField.textAlignment = textAlignment
-        textField.keyboardType = keyboardType
-        textField.autocapitalizationType = autocapitalizationType
-        textField.autocorrectionType = autocorrectionType
-
-        if shouldBecomeFirstResponder, !context.coordinator.didBecomeFirstResponder {
-            if textField.window != nil, textField.becomeFirstResponder() {
-                context.coordinator.didBecomeFirstResponder = true
+            .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
             }
-        } else if !shouldBecomeFirstResponder, context.coordinator.didBecomeFirstResponder {
-            context.coordinator.didBecomeFirstResponder = false
-        }
-    }
-
-    public func makeCoordinator() -> Coordinator {
-        Coordinator(self, maxLength: maxLength)
     }
 
-    public final class Coordinator: NSObject {
-        var parent: TextFieldWithToolBar
-        var textField: UITextField?
-        let maxLength: Int?
-        var didBecomeFirstResponder = false
-        let decimalFormatter: NumberFormatter
-
-        init(_ parent: TextFieldWithToolBar, maxLength: Int?) {
-            self.parent = parent
-            self.maxLength = maxLength
-            decimalFormatter = NumberFormatter()
-            decimalFormatter.locale = Locale.current
-            decimalFormatter.numberStyle = .decimal
+    private func handleTextChange(_ oldValue: String, _ newValue: String) {
+        // Handle empty input (clear operation)
+        if newValue.isEmpty {
+            text = 0
+            isZeroCleared = true
+            textDidChange?(0)
+            return
         }
 
-        @objc fileprivate func clearText() {
-            parent.text = 0
-            textField?.text = ""
+        // 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
         }
 
-        @objc fileprivate func editingDidBegin(_ textField: UITextField) {
-            DispatchQueue.main.async {
-                textField.moveCursorToEnd()
-            }
-        }
+        let currentDecimalSeparator = numberFormatter.decimalSeparator ?? "."
 
-        @objc fileprivate func previousTextField() {
-            parent.previousTextField?()
+        // Ensure there's only one decimal separator
+        let decimalSeparatorCount = newValue.filter { String($0) == currentDecimalSeparator }.count
+        if decimalSeparatorCount > 1 {
+            // Reject input with multiple decimal separators
+            localText = oldValue
+            return
         }
 
-        @objc fileprivate func nextTextField() {
-            parent.nextTextField?()
+        // Handle localization by converting to the correct decimal separator
+        var processedText = newValue
+        if newValue.contains("."), currentDecimalSeparator != "." {
+            processedText = newValue.replacingOccurrences(of: ".", with: currentDecimalSeparator)
+        } else if newValue.contains(","), currentDecimalSeparator != "," {
+            processedText = newValue.replacingOccurrences(of: ",", with: currentDecimalSeparator)
         }
 
-        // Helper method to calculate the number of decimal places in a string
-        fileprivate func calculateDecimalPlaces(in string: String) -> Int {
-            guard let decimalSeparator = decimalFormatter.decimalSeparator else { return 0 }
-            if let range = string.range(of: decimalSeparator) {
-                let decimalPart = string[range.upperBound...]
-                return decimalPart.count
-            }
-            return 0
+        // Automatically add leading zero when starting with decimal separator
+        // For example ".5" becomes "0.5"
+        if processedText.hasPrefix(currentDecimalSeparator) {
+            processedText = "0" + processedText
         }
 
-        // Helper method to check if the cursor is after the decimal separator
-        fileprivate func isCursorAfterDecimal(in textField: UITextField, range: NSRange) -> Bool {
-            guard let text = textField.text, let decimalSeparator = decimalFormatter.decimalSeparator else { return false }
-            if let decimalSeparatorRange = text.range(of: decimalSeparator) {
-                let decimalSeparatorPosition = text.distance(from: text.startIndex, to: decimalSeparatorRange.lowerBound)
-                return range.location > decimalSeparatorPosition
-            }
-            return false
-        }
-    }
-}
+        // Validate against number formatter digit limits
+        let components = processedText.components(separatedBy: currentDecimalSeparator)
 
-extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate {
-    public func textFieldDidEndEditing(_ textField: UITextField) {
-        if let text = textField.text,
-           let decimal = Decimal(string: text, locale: parent.numberFormatter.locale)
-        {
-            // Format the number properly when editing ends
-            textField.text = parent.numberFormatter.string(from: decimal as NSNumber)
-            parent.text = decimal
+        // 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
 
-    public func textField(
-        _ textField: UITextField,
-        shouldChangeCharactersIn range: NSRange,
-        replacementString string: String
-    ) -> Bool {
-        // Check if the input is a number or the decimal separator
-        let isNumber = CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string))
-
-        // Get the current locale's decimal separator
-        let currentDecimalSeparator = parent.numberFormatter.decimalSeparator ?? "."
-
-        // Check if input is a decimal separator (either . or ,)
-        let isInputDecimalSeparator = string == "." || string == ","
-
-        // Only allow the decimal separator configured in the locale
-        if isInputDecimalSeparator {
-            // If it's not the correct decimal separator for this locale, reject it
-            if string != currentDecimalSeparator {
-                return false
-            }
-            // Check if the field already contains a decimal separator
-            if textField.text?.contains(currentDecimalSeparator) == true {
-                return false
-            }
-        }
+        // Count fraction digits (after decimal separator)
+        let fractionDigits = components.count > 1 ? components[1].filter { $0.isNumber }.count : 0
 
-        // Only proceed if the input is a valid number or the correct decimal separator
-        if isNumber || (string == currentDecimalSeparator && parent.allowDecimalSeparator),
-           let currentText = textField.text as NSString?
+        // Validate against the formatter's digit limits
+        if integerDigits > numberFormatter.maximumIntegerDigits ||
+            (allowDecimalSeparator && fractionDigits > numberFormatter.maximumFractionDigits)
         {
-            // Calculate the new text length
-            let newLength = currentText.length + string.count - range.length
-
-            // Check max length if specified
-            if let maxLength = parent.maxLength, newLength > maxLength {
-                return false
-            }
-
-            // Create the new text string
-            let newText = currentText.replacingCharacters(in: range, with: string)
-
-            // If text starts with decimal separator, add leading zero
-            if newText.hasPrefix(currentDecimalSeparator) {
-                textField.text = "0" + newText
-                parent.text = Decimal(string: textField.text ?? "0") ?? 0
-                return false
-            }
+            // Reject input that exceeds digit limits
+            localText = oldValue
+            return
+        }
 
-            // Update the binding
-            if let decimal = Decimal(string: newText, locale: parent.numberFormatter.locale) {
-                parent.text = 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 we had to process/modify the input, update the displayed text
+                if processedText != newValue {
+                    localText = processedText
+                }
             }
-
-            return true
+        } else {
+            // Reject invalid decimal inputs
+            localText = oldValue
         }
-
-        // Allow the change if it's a valid number or the correct decimal separator
-        return isNumber || (string == currentDecimalSeparator && parent.allowDecimalSeparator)
-    }
-
-    public func textFieldDidBeginEditing(_: UITextField) {
-        parent.textFieldDidBeginEditing?()
     }
 }