Search code examples
swiftswiftuiuikitcustom-keyboarduiviewrepresentable

Custom TextField keyboard in SwiftUI with UIViewRepresentable


I'm using a custom UITextField inside a UIViewRepresentable, to have a Textfield with only a decimalpad and a custom keyboard toolbar. The goal is to add to the toolbar basic things such as inserting a minus, an E, etc... I've seen a lot of code that only adresses the "Done" button. I've tried adding insertText() to the button action but I think the Coordinator is not called and therefore the text is not updated. All I want is the ability of inserting a custom string on the cursor position.

Here is the code:

struct DataTextField: UIViewRepresentable {

private var placeholder: String
@Binding var text: String

 init(_ placeholder: String, text: Binding<String>) {
    self.placeholder = placeholder
    self._text = text
 }

 func makeUIView(context: Context) -> UITextField {
    let textfield = UITextField()
    textfield.keyboardType = .decimalPad
    textfield.delegate = context.coordinator
    textfield.placeholder = placeholder
    let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textfield.frame.size.width, height: 44))
    let minusButton = UIBarButtonItem(title: "-", style: .plain, target: self, action: #selector(textfield.minusButtonTapped(button:)))
    let scientificButton = UIBarButtonItem(title: "E", style: .plain, target: self, action: #selector(textfield.scientificButtonTapped(button:)))
    toolBar.items = [minusButton, scientificButton]
    toolBar.setItems([minusButton, scientificButton], animated: true)
    textfield.inputAccessoryView = toolBar
    textfield.borderStyle = .roundedRect
    textfield.textAlignment = .right
    textfield.adjustsFontSizeToFitWidth = true
    return textfield
    
 }
    
func updateUIView(_ uiView: UITextField, context: Context) {
    uiView.text = text
}

 func makeCoordinator() -> Coordinator {
    Coordinator(self)
 }

 class Coordinator: NSObject, UITextFieldDelegate {
    var parent: DataTextField

 init(_ textField: DataTextField) {
    self.parent = textField
 }

 func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
     if let currentValue = textField.text as NSString? {
         let proposedValue = currentValue.replacingCharacters(in: range, with: string) as String
         self.parent.text = proposedValue
     }
     return true
     }
   }
 }

extension  UITextField {
   @objc func minusButtonTapped(button:UIBarButtonItem) -> Void {
    insertText("-")
   }
    @objc func scientificButtonTapped(button:UIBarButtonItem) -> Void {
     insertText("E")
    }
}

And a video of the problem:

Keyboard issue, if I press a keyboard button after the minus, the minus keeps on the textfield.


Solution

  • I discovered that it is a Swift bug that only updates the text if its not inside an array. (In my case, it was) After hours trying, I discovered that calling delegate?.textFieldDidBeginEditing?(self) and then updating the text inside that function makes the buttons work as expected.

    extension UITextField {
        @objc func minusButtonTapped(button: UIBarButtonItem) -> Void {
            insertText("-")
            delegate?.textFieldDidBeginEditing?(self)
        }
        @objc func scientificButtonTapped(button: UIBarButtonItem) -> Void {
            insertText("E")
            delegate?.textFieldDidBeginEditing?(self)
        }
    }
    

    Add this to the Coordinator class inside the UIViewRepresentable:

        func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
            textField.resignFirstResponder()
        }
        
        func textFieldDidBeginEditing(_ textField: UITextField) {
            self.parent.text = textField.text!
        }
    

    Calling .resignFirstResponder() is a must. The app will crash if the keyboard is not dismissed, since in my case the view itself can be dismissed.