UITextField
iOS Development
Backspace Detection
Swift Programming
User Interface

Detect backspace in empty UITextField

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Detecting a backspace in an empty UITextField matters when the field is part of a custom input flow, such as an OTP screen or a token editor. The clean solution is not to guess from text changes after the fact, but to intercept the delete action directly.

Why the Delegate Method Is Not Enough

Many developers start with textField(_:shouldChangeCharactersIn:replacementString:). That method is useful for normal edits, but it is not the best hook when the field is already empty and you want special behavior such as moving focus to the previous field.

For regular deletion logic, it can still help:

swift
1import UIKit
2
3final class FormViewController: UIViewController, UITextFieldDelegate {
4    @IBOutlet private weak var textField: UITextField!
5
6    override func viewDidLoad() {
7        super.viewDidLoad()
8        textField.delegate = self
9    }
10
11    func textField(
12        _ textField: UITextField,
13        shouldChangeCharactersIn range: NSRange,
14        replacementString string: String
15    ) -> Bool {
16        let isBackspace = string.isEmpty && range.length == 1
17        if isBackspace {
18            print("Deleting existing text")
19        }
20        return true
21    }
22}

That covers normal deletion, but it does not express the intent clearly when you specifically care about delete on an empty field.

The Robust Solution: Override deleteBackward()

UITextField exposes deleteBackward(), which is the correct place to detect backspace regardless of whether there is text in the field.

swift
1import UIKit
2
3final class BackspaceTextField: UITextField {
4    var onEmptyBackspace: (() -> Void)?
5
6    override func deleteBackward() {
7        let wasEmpty = text?.isEmpty ?? true
8        super.deleteBackward()
9
10        if wasEmpty {
11            onEmptyBackspace?()
12        }
13    }
14}

This works because you record whether the field was empty before the normal delete behavior runs. If it was empty, you trigger your custom callback after allowing UIKit to process the event.

A Practical OTP Example

The most common use case is a one-character code entry UI where pressing backspace in an empty field should move focus to the previous field.

swift
1import UIKit
2
3final class OTPViewController: UIViewController {
4    @IBOutlet private weak var field1: BackspaceTextField!
5    @IBOutlet private weak var field2: BackspaceTextField!
6    @IBOutlet private weak var field3: BackspaceTextField!
7
8    override func viewDidLoad() {
9        super.viewDidLoad()
10
11        field2.onEmptyBackspace = { [weak self] in
12            self?.field1.becomeFirstResponder()
13        }
14
15        field3.onEmptyBackspace = { [weak self] in
16            self?.field2.becomeFirstResponder()
17        }
18    }
19}

This is much clearer than trying to infer empty-field backspace from delegate edge cases. It also keeps the control-specific behavior in the control subclass where it belongs.

Combine It with Normal Input Handling

Overriding deleteBackward() does not replace your normal text validation. You still use delegates or target-action for formatting, length limits, and advancing to the next field.

swift
1import UIKit
2
3final class OTPViewController: UIViewController, UITextFieldDelegate {
4    @IBOutlet private weak var field1: BackspaceTextField!
5    @IBOutlet private weak var field2: BackspaceTextField!
6
7    override func viewDidLoad() {
8        super.viewDidLoad()
9        field1.delegate = self
10        field2.delegate = self
11    }
12
13    func textField(
14        _ textField: UITextField,
15        shouldChangeCharactersIn range: NSRange,
16        replacementString string: String
17    ) -> Bool {
18        guard string.count <= 1 else { return false }
19
20        if !string.isEmpty {
21            textField.text = string
22            if textField === field1 {
23                field2.becomeFirstResponder()
24            }
25            return false
26        }
27
28        return true
29    }
30}

The delegate manages forward motion and text replacement. The subclass handles the empty-backspace case. The responsibilities stay separate and easier to test.

When You Need to Support Paste or Hardware Keyboards

OTP and token fields often receive paste events or input from hardware keyboards. deleteBackward() still works well for delete handling, but your delegate logic should also be ready for multi-character pastes.

That means validating replacementString, splitting pasted content if needed, and not assuming every input event is a single tap on the software keyboard.

Common Pitfalls

The most common mistake is trying to detect empty-field backspace only through shouldChangeCharactersIn. That can appear to work in one scenario and fail in another because it is not the most direct hook.

Another mistake is forgetting to call super.deleteBackward(). If you skip it, the field can behave differently from a normal UITextField.

Developers also capture self strongly in callbacks, which can keep the view controller alive longer than intended. Use [weak self] when the callback points back to the controller.

Finally, do not put every input rule inside the subclass. Keep the subclass focused on delete behavior and use delegates or view models for the rest of the form logic.

Summary

  • The cleanest way to detect backspace on an empty UITextField is to subclass and override deleteBackward().
  • 'shouldChangeCharactersIn is still useful for ordinary editing, but it is not the best abstraction for empty-field delete events.'
  • Empty-backspace handling is especially useful in OTP and multi-field entry UIs.
  • Call super.deleteBackward() so the control keeps UIKit's normal behavior.
  • Separate delete detection from the rest of your validation and focus-management logic.

Course illustration
Course illustration

All Rights Reserved.