Fix: Missing textFieldDidChange in UITextFieldDelegate

Fix: Missing textFieldDidChange in UITextFieldDelegate

Written by

in

Quick answer: UITextFieldDelegate does not have a textFieldDidChange method because UITextField inherits from UIControl. To detect text changes as a user types, call addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) or listen for UITextField.textDidChangeNotification.

Why this happens

Developers migrating from UITextView often expect UITextFieldDelegate to expose a textFieldDidChange method that mirrors textViewDidChange(_:). When inspecting UITextFieldDelegate, however, no such callback exists.

This design difference comes down to the UIKit class hierarchy. UITextView inherits directly from UIScrollView (and UIView), so it must route all state updates through its delegate protocol. UITextField, by contrast, inherits from UIControl. Apple designed UIControl subclasses—such as UIButton, UISwitch, and UITextField—to emit events through the target-action mechanism (UIControl.Event).

Because text updates are dispatched as .editingChanged control events, UITextFieldDelegate is reserved strictly for gating and intercepting changes (such as textField(_:shouldChangeCharactersIn:replacementString:)) or handling focus transitions (textFieldDidBeginEditing(_:) and textFieldDidEndEditing(_:)). If you mistakenly hook your button validation logic into textFieldDidBeginEditing(_:), the code runs as soon as the user taps into the field, long before any characters are typed.

How to fix it

1. Add an Target for .editingChanged Programmatically

The standard UIKit pattern for observing character changes in a UITextField is attaching a target for the .editingChanged control event. This event fires every time the user inserts, replaces, or deletes text via the keyboard or dictation.

import UIKit

class LoginViewController: UIViewController {
    private let usernameField = UITextField()
    private let passwordField = UITextField()
    private let submitButton = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupActions()
        updateSubmitButtonState()
    }

    private func setupUI() {
        submitButton.setTitle("Log In", for: .normal)
        submitButton.isEnabled = false
    }

    private func setupActions() {
        // Register targets for the .editingChanged event
        usernameField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
        passwordField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
    }

    @objc private func textFieldDidChange(_ textField: UITextField) {
        updateSubmitButtonState()
    }

    private func updateSubmitButtonState() {
        let username = usernameField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        let password = passwordField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""

        submitButton.isEnabled = !username.isEmpty && !password.isEmpty
    }
}

This callback receives the updated UITextField as its argument, and textField.text already reflects the newly entered characters.

2. Connect the Editing Changed Action in Interface Builder

If you build your interface using Storyboards or XIB files, you can connect the action without writing addTarget manually in code:

  1. Open your storyboard or .xib file in Xcode.
  2. Open the Assistant Editor side-by-side with your UIViewController subclass.
  3. Hold Control and drag from the UITextField into the body of your view controller class.
  4. In the connection popup, change the Connection style from Outlet to Action.
  5. Enter a name (such as textFieldDidChange).
  6. Ensure the Event dropdown is set to Editing Changed (not Editing Did Begin or Value Changed).
  7. Click Connect.

Xcode generates an @IBAction method in your file:

@IBAction func textFieldDidChange(_ sender: UITextField) {
    let currentText = sender.text ?? ""
    submitButton.isEnabled = !currentText.isEmpty
}

3. Subscribe to UITextField.textDidChangeNotification

When your observation logic lives outside the immediate view controller—such as in a coordinator, form manager, or logging module—NotificationCenter is an alternative that decouples the observer from the control instance.

import UIKit

class FormObserver {
    private var observerToken: NSObjectProtocol?

    func observe(textField: UITextField, onTextChange: @escaping (String) -> Void) {
        // Pass the specific textField instance as the object to avoid listening to all fields
        observerToken = NotificationCenter.default.addObserver(
            forName: UITextField.textDidChangeNotification,
            object: textField,
            queue: .main
        ) { notification in
            if let field = notification.object as? UITextField {
                onTextChange(field.text ?? "")
            }
        }
    }

    deinit {
        if let token = observerToken {
            NotificationCenter.default.removeObserver(token)
        }
    }
}

Always pass the specific UITextField instance into the object: parameter unless you intentionally want to monitor every text field across the entire application.

4. Use a Combine Publisher

In projects using modern reactive patterns, NotificationCenter.Publisher wraps the text field notification into a Combine pipeline, allowing you to debounce rapid typing before running expensive operations such as search queries.

import UIKit
import Combine

class SearchViewController: UIViewController {
    private let searchField = UITextField()
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: searchField)
            .compactMap { ($0.object as? UITextField)?.text }
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .sink { [weak self] query in
                self?.performSearch(query: query)
            }
            .store(in: &cancellables)
    }

    private func performSearch(query: String) {
        // Execute network or database query
    }
}

How to prevent it

  • Remember that UITextField is a UIControl; use .editingChanged targets rather than hunting for delegate methods.
  • Avoid using textFieldDidBeginEditing(_:) for form validation; that delegate hook only signals that the field received focus.
  • Reserve textField(_:shouldChangeCharactersIn:replacementString:) strictly for input masking or character limits, not state updates.
  • Store any NotificationCenter observer tokens or Combine cancellables properly so observers do not leak when controllers are dismissed.

FAQ

Does changing textField.text programmatically fire .editingChanged?

No. Assigning a value directly to textField.text in code does not trigger .editingChanged or post UITextField.textDidChangeNotification. To run your change logic after a programmatic assignment, call your handler directly or invoke textField.sendActions(for: .editingChanged).

Why not use textField(_:shouldChangeCharactersIn:replacementString:) to read changes?

That delegate method is called before the change is actually committed to the field, meaning textField.text still contains the old string. While you can construct the new string using (textField.text as NSString?)?.replacingCharacters(in: range, with: string), .editingChanged is cleaner because the property is already updated.

Can I listen to text changes across all text fields on a screen at once?

Yes. Subscribe to UITextField.textDidChangeNotification with object: nil. UIKit broadcasts this notification for every text field in the app, and you can inspect notification.object as? UITextField to determine which field triggered the event.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.