DecimalTextField.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import SwiftUI
  2. import UIKit
  3. public struct TextFieldWithToolBar: UIViewRepresentable {
  4. @Binding var text: Decimal
  5. var placeholder: String
  6. var textColor: UIColor
  7. var textAlignment: NSTextAlignment
  8. var keyboardType: UIKeyboardType
  9. var autocapitalizationType: UITextAutocapitalizationType
  10. var autocorrectionType: UITextAutocorrectionType
  11. var shouldBecomeFirstResponder: Bool
  12. var maxLength: Int?
  13. var isDismissible: Bool
  14. var textFieldDidBeginEditing: (() -> Void)?
  15. var numberFormatter: NumberFormatter
  16. public init(
  17. text: Binding<Decimal>,
  18. placeholder: String,
  19. textColor: UIColor = .label,
  20. textAlignment: NSTextAlignment = .right,
  21. keyboardType: UIKeyboardType = .decimalPad,
  22. autocapitalizationType: UITextAutocapitalizationType = .none,
  23. autocorrectionType: UITextAutocorrectionType = .no,
  24. shouldBecomeFirstResponder: Bool = false,
  25. maxLength: Int? = nil,
  26. isDismissible: Bool = true,
  27. textFieldDidBeginEditing: (() -> Void)? = nil,
  28. numberFormatter: NumberFormatter
  29. ) {
  30. _text = text
  31. self.placeholder = placeholder
  32. self.textColor = textColor
  33. self.textAlignment = textAlignment
  34. self.keyboardType = keyboardType
  35. self.autocapitalizationType = autocapitalizationType
  36. self.autocorrectionType = autocorrectionType
  37. self.shouldBecomeFirstResponder = shouldBecomeFirstResponder
  38. self.maxLength = maxLength
  39. self.isDismissible = isDismissible
  40. self.textFieldDidBeginEditing = textFieldDidBeginEditing
  41. self.numberFormatter = numberFormatter
  42. self.numberFormatter.numberStyle = .decimal
  43. }
  44. public func makeUIView(context: Context) -> UITextField {
  45. let textField = UITextField()
  46. context.coordinator.textField = textField
  47. textField.inputAccessoryView = isDismissible ? makeDoneToolbar(for: textField, context: context) : nil
  48. // textField.addTarget(context.coordinator, action: #selector(Coordinator.textChanged), for: .editingChanged)
  49. textField.addTarget(context.coordinator, action: #selector(Coordinator.editingDidBegin), for: .editingDidBegin)
  50. textField.delegate = context.coordinator
  51. textField.text = numberFormatter.string(for: text)
  52. return textField
  53. }
  54. private func makeDoneToolbar(for textField: UITextField, context: Context) -> UIToolbar {
  55. let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
  56. let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
  57. let doneButton = UIBarButtonItem(
  58. image: UIImage(systemName: "keyboard.chevron.compact.down"),
  59. style: .done,
  60. target: textField,
  61. action: #selector(UITextField.resignFirstResponder)
  62. )
  63. let clearButton = UIBarButtonItem(
  64. image: UIImage(systemName: "trash"),
  65. style: .plain,
  66. target: context.coordinator,
  67. action: #selector(Coordinator.clearText)
  68. )
  69. toolbar.items = [clearButton, flexibleSpace, doneButton]
  70. toolbar.sizeToFit()
  71. return toolbar
  72. }
  73. public func updateUIView(_ textField: UITextField, context: Context) {
  74. if text != 0 {
  75. let newText = numberFormatter.string(for: text) ?? ""
  76. if textField.text != newText {
  77. textField.text = newText
  78. }
  79. } else {
  80. textField.text = ""
  81. }
  82. textField.placeholder = placeholder
  83. textField.textColor = textColor
  84. textField.textAlignment = textAlignment
  85. textField.keyboardType = keyboardType
  86. textField.autocapitalizationType = autocapitalizationType
  87. textField.autocorrectionType = autocorrectionType
  88. if shouldBecomeFirstResponder, !context.coordinator.didBecomeFirstResponder {
  89. if textField.window != nil, textField.becomeFirstResponder() {
  90. context.coordinator.didBecomeFirstResponder = true
  91. }
  92. } else if !shouldBecomeFirstResponder, context.coordinator.didBecomeFirstResponder {
  93. context.coordinator.didBecomeFirstResponder = false
  94. }
  95. }
  96. public func makeCoordinator() -> Coordinator {
  97. Coordinator(self, maxLength: maxLength)
  98. }
  99. public final class Coordinator: NSObject {
  100. var parent: TextFieldWithToolBar
  101. var textField: UITextField?
  102. let maxLength: Int?
  103. var didBecomeFirstResponder = false
  104. init(_ parent: TextFieldWithToolBar, maxLength: Int?) {
  105. self.parent = parent
  106. self.maxLength = maxLength
  107. }
  108. // @objc fileprivate func textChanged(_ textField: UITextField) {
  109. // if let text = textField.text, let value = parent.numberFormatter.number(from: text)?.decimalValue {
  110. // parent.text = value
  111. // } else {
  112. // parent.text = 0
  113. // }
  114. // }
  115. @objc fileprivate func clearText() {
  116. parent.text = 0
  117. textField?.text = ""
  118. }
  119. @objc fileprivate func editingDidBegin(_ textField: UITextField) {
  120. DispatchQueue.main.async {
  121. textField.moveCursorToEnd()
  122. }
  123. }
  124. }
  125. }
  126. extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate {
  127. public func textField(
  128. _ textField: UITextField,
  129. shouldChangeCharactersIn range: NSRange,
  130. replacementString string: String
  131. ) -> Bool {
  132. // Allow only numbers and decimal characters
  133. let isNumber = CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string))
  134. let withDecimal = (
  135. string == NumberFormatter().decimalSeparator &&
  136. textField.text?.contains(string) == false
  137. )
  138. if isNumber || withDecimal,
  139. let currentValue = textField.text as NSString?
  140. {
  141. // Update Value
  142. let proposedValue = currentValue.replacingCharacters(in: range, with: string) as String
  143. let decimalFormatter = NumberFormatter()
  144. decimalFormatter.locale = Locale.current
  145. decimalFormatter.numberStyle = .decimal
  146. // Try currency formatter then Decimal formatrer
  147. let number = parent.numberFormatter.number(from: proposedValue) ?? decimalFormatter
  148. .number(from: proposedValue) ?? 0.0
  149. // Set Value
  150. let double = number.doubleValue
  151. parent.text = Decimal(double)
  152. }
  153. return isNumber || withDecimal
  154. }
  155. public func textFieldDidBeginEditing(_: UITextField) {
  156. parent.textFieldDidBeginEditing?()
  157. }
  158. }
  159. extension UITextField {
  160. func moveCursorToEnd() {
  161. dispatchPrecondition(condition: .onQueue(.main))
  162. let newPosition = endOfDocument
  163. selectedTextRange = textRange(from: newPosition, to: newPosition)
  164. }
  165. }
  166. extension UIApplication {
  167. func endEditing() {
  168. sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
  169. }
  170. }