import SwiftUI import UIKit public struct TextFieldWithToolBar: UIViewRepresentable { @Binding var text: Decimal var placeholder: String var textColor: UIColor var textAlignment: NSTextAlignment var keyboardType: UIKeyboardType var autocapitalizationType: UITextAutocapitalizationType var autocorrectionType: UITextAutocorrectionType var shouldBecomeFirstResponder: Bool var maxLength: Int? var isDismissible: Bool var textFieldDidBeginEditing: (() -> Void)? var numberFormatter: NumberFormatter var allowDecimalSeparator: Bool public init( text: Binding, placeholder: String, textColor: UIColor = .label, textAlignment: NSTextAlignment = .right, keyboardType: UIKeyboardType = .decimalPad, autocapitalizationType: UITextAutocapitalizationType = .none, autocorrectionType: UITextAutocorrectionType = .no, shouldBecomeFirstResponder: Bool = false, maxLength: Int? = nil, isDismissible: Bool = true, textFieldDidBeginEditing: (() -> Void)? = nil, numberFormatter: NumberFormatter, allowDecimalSeparator: Bool = true ) { _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.isDismissible = isDismissible self.textFieldDidBeginEditing = textFieldDidBeginEditing self.numberFormatter = numberFormatter self.numberFormatter.numberStyle = .decimal self.allowDecimalSeparator = allowDecimalSeparator } public func makeUIView(context: Context) -> UITextField { let textField = UITextField() context.coordinator.textField = textField textField.inputAccessoryView = isDismissible ? makeDoneToolbar(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 } private func makeDoneToolbar(for textField: UITextField, context: Context) -> UIToolbar { let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50)) let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let doneButton = UIBarButtonItem( image: UIImage(systemName: "keyboard.chevron.compact.down"), style: .done, target: textField, action: #selector(UITextField.resignFirstResponder) ) let clearButton = UIBarButtonItem( image: UIImage(systemName: "trash"), style: .plain, target: context.coordinator, action: #selector(Coordinator.clearText) ) toolbar.items = [clearButton, flexibleSpace, doneButton] 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 } } 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 } } 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 } @objc fileprivate func clearText() { parent.text = 0 textField?.text = "" } @objc fileprivate func editingDidBegin(_ textField: UITextField) { DispatchQueue.main.async { textField.moveCursorToEnd() } } } } extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate { 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)) let isDecimalSeparator = (string == decimalFormatter.decimalSeparator && textField.text?.contains(string) == false) // Only proceed if the input is a valid number or decimal separator if isNumber || isDecimalSeparator && parent.allowDecimalSeparator, let currentText = textField.text as NSString? { // Get the proposed new text let proposedText = currentText.replacingCharacters(in: range, with: string) // Try to convert proposed text to number let number = parent.numberFormatter.number(from: proposedText) ?? decimalFormatter.number(from: proposedText) // Update the binding value if conversion is successful if let number = number { parent.text = number.decimalValue } else { parent.text = 0 } } // Allow the change if it's a valid number or decimal separator return isNumber || isDecimalSeparator && parent.allowDecimalSeparator } public func textFieldDidBeginEditing(_: UITextField) { parent.textFieldDidBeginEditing?() } } extension UITextField { func moveCursorToEnd() { dispatchPrecondition(condition: .onQueue(.main)) let newPosition = endOfDocument selectedTextRange = textRange(from: newPosition, to: newPosition) } } extension UIApplication { func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } public struct TextFieldWithToolBarString: UIViewRepresentable { @Binding var text: String var placeholder: String var textAlignment: NSTextAlignment = .right var keyboardType: UIKeyboardType = .default var autocapitalizationType: UITextAutocapitalizationType = .none var autocorrectionType: UITextAutocorrectionType = .no var shouldBecomeFirstResponder: Bool = false var maxLength: Int? = nil var isDismissible: Bool = true public func makeUIView(context: Context) -> UITextField { let textField = UITextField() context.coordinator.textField = textField textField.inputAccessoryView = isDismissible ? makeDoneToolbar(for: textField, context: context) : nil textField.addTarget(context.coordinator, action: #selector(Coordinator.editingDidBegin), for: .editingDidBegin) textField.delegate = context.coordinator textField.text = text textField.placeholder = placeholder textField.textAlignment = textAlignment textField.keyboardType = keyboardType textField.autocapitalizationType = autocapitalizationType textField.autocorrectionType = autocorrectionType textField.adjustsFontSizeToFitWidth = true return textField } private func makeDoneToolbar(for textField: UITextField, context: Context) -> UIToolbar { let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50)) let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let doneButton = UIBarButtonItem( image: UIImage(systemName: "keyboard.chevron.compact.down"), style: .done, target: textField, action: #selector(UITextField.resignFirstResponder) ) let clearButton = UIBarButtonItem( image: UIImage(systemName: "trash"), style: .plain, target: context.coordinator, action: #selector(Coordinator.clearText) ) toolbar.items = [clearButton, flexibleSpace, doneButton] toolbar.sizeToFit() return toolbar } public func updateUIView(_ textField: UITextField, context: Context) { if textField.text != text { textField.text = text } 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 } } 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: TextFieldWithToolBarString var textField: UITextField? let maxLength: Int? var didBecomeFirstResponder = false init(_ parent: TextFieldWithToolBarString, maxLength: Int?) { self.parent = parent self.maxLength = maxLength } @objc fileprivate func clearText() { parent.text = "" textField?.text = "" } @objc fileprivate func editingDidBegin(_ textField: UITextField) { DispatchQueue.main.async { textField.moveCursorToEnd() } } } } extension TextFieldWithToolBarString.Coordinator: UITextFieldDelegate { public func textField( _ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String ) -> Bool { if let maxLength = parent.maxLength { // Get the current text, including the proposed change let currentText = textField.text ?? "" let newLength = currentText.count + string.count - range.length if newLength > maxLength { return false } } DispatchQueue.main.async { if let textFieldText = textField.text as NSString? { let newText = textFieldText.replacingCharacters(in: range, with: string) self.parent.text = newText } } return true } }