Detect backspace in empty UITextField

Viewed 80522

Is there any way to detect when the Backspace/Delete key is pressed in the iPhone keyboard on a UITextField that is empty? I want to know when Backspace is pressed only if the UITextField is empty.


Based on the suggestion from @Alex Reynolds in a comment, I've added the following code while creating my text field:

[[NSNotificationCenter defaultCenter] addObserver:self
          selector:@selector(handleTextFieldChanged:)
              name:UITextFieldTextDidChangeNotification
            object:searchTextField];

This notification is received (handleTextFieldChanged function is called), but still not when I press the Backspace key in an empty field. Any ideas?


There seems to be some confusion around this question. I want to receive a notification when the Backspace key is pressed. That's it. But the solution must also work when the UITextField is already empty.

28 Answers

This may be a long shot but it could work. Try setting the text field's text to a zero width space character \u200B. When backspace is pressed on a text field that appears empty, it will actually delete your space. Then you can just reinsert the space.

May not work if the user manages to move the caret to the left of the space.

I have implemented the similar solution with minor improvements that will tell me that if the text field has any value while the user has tapped the backspace. This is useful for my case when I should only focus on another text field if the text field is empty when backspace pressed.

protocol MyTextFieldDelegate : UITextFieldDelegate {
    func textFieldDidDelete(textField: MyTextField, hasValue: Bool)
}

override func deleteBackward() {
    let currentText = self.text ?? ""
    super.deleteBackward()
    let hasValue = currentText.isEmpty ? false : true
    if let delegate = self.delegate as? MyTextFieldDelegate {
        delegate.textFieldDidDelete(textField: self, hasValue: hasValue)
    }
}

The most poplar answer is missing one thing — the ability to detect whether the text field was empty or not.

That is, when you override the deleteBackwards() method of a TextField subclass, you still don't know whether the text field was already empty. (Both before and after deleteBackwards(), textField.text! is an empty string: "")

Here's my improvement, with a check for emptiness prior to deletion.

1. Create a delegate protocol that extends UITextFieldDelegate

protocol MyTextFieldDelegate: UITextFieldDelegate {
    func textField(_ textField: UITextField, didDeleteBackwardAnd wasEmpty: Bool)
}

2. Subclass UITextField

class MyTextField: UITextField {
    override func deleteBackward() {
        // see if text was empty
        let wasEmpty = text == nil || text! == ""

        // then perform normal behavior
        super.deleteBackward()

        // now, notify delegate (if existent)
        (delegate as? MyTextFieldDelegate)?.textField(self, didDeleteBackwardAnd: wasEmpty)
    }
}

3. Implement your new delegate protocol

extension MyViewController: MyTextFieldDelegate {
    func textField(_ textField: UITextField, didDeleteBackwardAnd wasEmpty: Bool) {
        if wasEmpty {
            // do what you want here...
        }
    }
}

Comprehensive handler for textfield with single digit number for Swift 5.1:

  • Assuming that you have outlet collection of textFields (with connected delegates as well)

1 Step

protocol MyTextFieldDelegate: class {
    func textField(_ textField: UITextField, didDeleteBackwardAnd wasEmpty: Bool) 
}

final class MyTextField: UITextField {

    weak var myDelegate: MyTextFieldDelegate?

    override func deleteBackward() {
        let wasEmpty = text == nil || text == ""

        // then perform normal behavior
        super.deleteBackward()

        // now, notify delegate (if existent)
        (delegate as? MyTextFieldDelegate)?.textField(self, didDeleteBackwardAnd: wasEmpty)
    }
}

2 Step

final class ViewController: UIViewController {

    @IBOutlet private var textFields: [MyTextField]!

    override func viewDidLoad() {
        super.viewDidLoad()
        textFields.forEach {
            $0.delegate = self
            $0.myDelegate = self
        }
    }
}

3 Step

extension ViewController: UITextFieldDelegate, MyTextFieldDelegate {
    func textFieldHasChanged(with text: String, _ tag: Int, for textField: UITextField) {
        textField.text = text

        if let someTextField = (textFields.filter { $0.tag == tag }).first {
            someTextField.becomeFirstResponder()
        } else {
            view.endEditing(true)
        }
    }

    func textField(_ textField: UITextField, didDeleteBackwardAnd wasEmpty: Bool) {
        // If the user was pressing backward and the value was empty, go to previous textField
        textFieldHasChanged(with: "", textField.tag - 1, for: textField)
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        // Restrict to only digits
        let aSet = NSCharacterSet(charactersIn: "0123456789").inverted
        let compSepByCharInSet = string.components(separatedBy: aSet)
        let numberFiltered = compSepByCharInSet.joined(separator: "")

        guard string == numberFiltered, let text = textField.text else { return false }

        if text.count >= 1 && string.isEmpty {
            // If the user is deleting the value
            textFieldHasChanged(with: "", textField.tag - 1, for: textField)
        } else {
            textFieldHasChanged(with: string, textField.tag + 1, for: textField)
        }

        return false
    }
}

Here my solution based on @andrew idea:

somewhere, for example in viewDidLoad

        textField.delegate = self
        textField.addTarget(self, action: #selector(valueChanged(_:)), for: .editingDidBegin)

and then

    @objc func valueChanged(_ textField: UITextField) {
        textField.text = "\u{200B}"
    }

    override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        textField.text = string
        if string == "" {
            //backpaspace pressed 
        }

All the answers are very helpful and I don't know why everyone is taking the protocol route. You can do it with much less code with call back function like-

Swift 5.0 or above

  1. Make a custom textfield class extending the UITextField and override the deleteBackward function-

    class CustomTextField: UITextField {

     var backButtonPressedInEmptyTextField: (()->())?
    
     override func deleteBackward() {
         super.deleteBackward()
         if let text = self.text, text.count == 0{
             backButtonPressedInEmptyTextField?()
             print("Back space clicked when textfield is empty")
         }
     }
    

    }

  2. Let's assume you want to do something based on that in your ViewController, MyViewController. So, in the MyViewController, just do following-

    class MyViewController: UIViewController{ @IBOutlet weak var sampleTextField: CustomTextField!{ didSet{ sampleTextField.backButtonPressedInEmptyTextField = backButtonPressed() } }

     func backButtonPressed(){
         //do whatever you want
     }
    

    }

I feel with closure or call-back function, it is much cleaner.

You can check the text of the text view/field to see if it's empty and make sure the replacement text is also empty in the shouldChangeTextIn delegate method.

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    if (textView.text == "" && text == "") {
        print("Backspace on empty text field.")
    }
    return true
}
Related