|
|
@@ -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?()
|
|
|
}
|
|
|
}
|
|
|
|