Введение: Почему "поймать" изменение текста — это база?
Представьте: вы пишете приложение для поиска рецептов. Пользователь начинает вводить "паста", а ваш поиск должен срабатывать мгновенно, без нажатия кнопки. Или вы делаете форму регистрации, где нужно подсвечивать ошибку "пароль слишком короткий" сразу, как только пользователь дописал третью букву.
В обоих случаях вам нужно знать точный момент, когда изменился текст в UITextField. Этот вопрос на Stack Overflow собрал тысячи просмотров — и не зря. Каждый iOS-разработчик рано или поздно с ним сталкивается.
В 2025 году у нас есть как минимум 4 способа решить эту задачу. Одни — старожилы, прошедшие проверку временем. Другие — свежие подходы, которые Apple активно продвигает. В этой статье мы разберем все варианты, от классики до модерна, и я покажу, какой способ выбрать под конкретную задачу.
Поехали! 🚀
Способ 1. Target-Action: Дедовский, но рабочий
Самый простой и понятный метод, который работает с iOS 2.0. Да-да, ему больше 15 лет, но он до сих пор актуален.
Идея в том, что UITextField наследуется от UIControl, а значит, умеет генерировать события. Нас интересует событие .editingChanged — оно срабатывает при каждом изменении текста.
Пример кода (UIKit + Storyboard или кодом)
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var searchTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Добавляем цель для события изменения текста
searchTextField.addTarget(self,
action: #selector(textFieldDidChange(_:)),
for: .editingChanged)
}
@objc func textFieldDidChange(_ textField: UITextField) {
guard let text = textField.text else { return }
print("Текст изменился: \(text)")
// Здесь можно делать поиск, валидацию и т.д.
}
}
Плюсы: минимум кода, не требует подписывания на протоколы, работает быстро.
Минусы: нужно использовать @objc — это legacy Swift. Нет тонкого контроля (например, нельзя отличить вставку текста от удаления).
Способ 2. Делегат UITextFieldDelegate: Контроль над каждым символом
Если вам нужно не просто узнать, что текст изменился, а повлиять на изменение (заблокировать ввод, отформатировать номер телефона), то делегат — ваш выбор.
Здесь ключевой метод — textField(_:shouldChangeCharactersIn:replacementString:). Он вызывается до того, как текст реально изменится.
Пример: форматирование номера телефона
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
@IBOutlet weak var phoneTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
phoneTextField.delegate = self
}
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
// Разрешаем только цифры
let allowedCharacters = CharacterSet.decimalDigits
let characterSet = CharacterSet(charactersIn: string)
guard allowedCharacters.isSuperset(of: characterSet) else {
return false
}
// Ограничим длину 10 символами
let currentText = textField.text ?? ""
guard let stringRange = Range(range, in: currentText) else { return false }
let updatedText = currentText.replacingCharacters(in: stringRange, with: string)
return updatedText.count <= 10
}
// Дополнительно: ловим момент, когда текст уже изменился
func textFieldDidChangeSelection(_ textField: UITextField) {
print("Текст обновлен: \(textField.text ?? "")")
}
}
Важно! В iOS 13+ появился метод textFieldDidChangeSelection — он срабатывает после изменения текста и выделения. Это более "чистый" аналог .editingChanged, но уже в протоколе делегата.
Плюсы: максимальный контроль, можно валидировать в реальном времени.
Минусы: нужно не забыть установить delegate, код становится объемнее.
Способ 3. Combine: Реактивный подход для iOS 13+
Если вы пишете современное приложение и используете Combine, то забудьте про делегаты. Apple предоставляет publisher для NotificationCenter, который идеально подходит для этой задачи.
Пример: реактивный поиск с debounce
import UIKit
import Combine
class SearchViewController: UIViewController {
@IBOutlet weak var searchField: UITextField!
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// Создаем publisher на изменение текста
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: searchField)
.compactMap { ($0.object as? UITextField)?.text }
.debounce(for: .milliseconds(300), scheduler: RunLoop.main) // ждем 0.3 сек после последнего ввода
.removeDuplicates() // не шлем запрос, если текст не изменился
.sink { [weak self] searchText in
print("Поиск: \(searchText)")
// Отправляем запрос на сервер
self?.performSearch(with: searchText)
}
.store(in: &cancellables)
}
private func performSearch(with query: String) {
// Ваша логика поиска
}
}
Обратите внимание на debounce — это "золотая пуля" для поисковых строк. Пока пользователь печатает, запросы не отправляются. Как только он делает паузу в 300 мс — запрос уходит. Это спасает и сервер, и батарею.
Плюсы: декларативный стиль, легко комбинировать с другими publisher, встроенная обработка задержек и фильтрация.
Минусы: требует iOS 13+, Combine — отдельная парадигма, не все разработчики с ней знакомы.
Способ 4. SwiftUI: onChange и @Binding (для справки)
Хотя статья про UIKit, нельзя игнорировать SwiftUI. Если вы используете SwiftUI, то все еще проще:
import SwiftUI
struct SearchView: View {
@State private var searchText = ""
var body: some View {
TextField("Введите запрос...", text: $searchText)
.onChange(of: searchText) { oldValue, newValue in
print("Текст изменился с \(oldValue) на \(newValue)")
// Ваша логика
}
}
}
В SwiftUI 5 (iOS 17+) у onChange появились два параметра — старое и новое значение. Это удобно для сравнения.
Плюсы: минималистично, SwiftUI-native.
Минусы: работает только в SwiftUI, не подходит для UIKit проектов.