Что такое паттерн delegate в UIKit и почему он широко используется?
Delegate — протокол, через который объект сообщает о событиях без жёсткой связи. UITableViewDelegate, UITextFieldDelegate и т.д. используют его, чтобы разделить логику отображения и бизнес-логику.
Что такое delegate pattern
Delegate pattern позволяет одному объекту делегировать часть своего поведения другому объекту, который реализует заданный протокол. Это разновидность composition over inheritance: вместо того чтобы наследоваться от UITableView и переопределять методы, вы реализуете протокол UITableViewDelegate и передаёте себя в свойство tableView.delegate.
UIKit использует этот паттерн повсеместно: UITableViewDataSource, UITextFieldDelegate, UIScrollViewDelegate, CLLocationManagerDelegate, URLSessionDelegate — всё это делегаты.
Рабочий пример: кастомный делегат
import UIKit
// 1. Объявляем протокол
protocol LoginFormDelegate: AnyObject {
func loginForm(_ form: LoginFormView, didSubmitEmail email: String, password: String)
func loginFormDidTapForgotPassword(_ form: LoginFormView)
}
// 2. Компонент хранит делегат слабой ссылкой
final class LoginFormView: UIView {
weak var delegate: LoginFormDelegate? // weak — чтобы избежать retain cycle
private let emailField = UITextField()
private let passwordField = UITextField()
private let submitButton = UIButton(type: .system)
private let forgotButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
submitButton.addTarget(self, action: #selector(handleSubmit), for: .touchUpInside)
forgotButton.addTarget(self, action: #selector(handleForgot), for: .touchUpInside)
}
required init?(coder: NSCoder) { fatalError() }
@objc private func handleSubmit() {
guard
let email = emailField.text, !email.isEmpty,
let password = passwordField.text, !password.isEmpty
else { return }
delegate?.loginForm(self, didSubmitEmail: email, password: password)
}
@objc private func handleForgot() {
delegate?.loginFormDidTapForgotPassword(self)
}
}
// 3. Контроллер становится делегатом
final class AuthViewController: UIViewController, LoginFormDelegate {
private let loginForm = LoginFormView()
override func viewDidLoad() {
super.viewDidLoad()
loginForm.delegate = self // назначаем себя
view.addSubview(loginForm)
}
// MARK: — LoginFormDelegate
func loginForm(_ form: LoginFormView, didSubmitEmail email: String, password: String) {
Task {
do {
try await AuthService.shared.login(email: email, password: password)
// навигация на главный экран
} catch {
showAlert(error.localizedDescription)
}
}
}
func loginFormDidTapForgotPassword(_ form: LoginFormView) {
let vc = ForgotPasswordViewController()
present(vc, animated: true)
}
}
Почему UIKit выбрал этот паттерн
Инверсия зависимости — UITableView ничего не знает о вашей бизнес-логике; он работает через абстрактный протокол. Это позволяет переиспользовать компонент в разных контекстах.
Loose coupling — компонент и его делегат можно менять независимо. Ни один из них не импортирует модули другого напрямую.
Контроль над потоком выполнения — делегат вызывается синхронно в момент события, что даёт предсказуемый порядок действий в main thread.
Ограниченная зона ответственности — компонент занимается отображением, делегат — реакцией на события. Нарушение этой границы (логика в UIView) — типичный запах плохого кода в UIKit-проектах.
Альтернативы и когда их выбирать
Closure/callback удобен для одного события без обратной связи. NotificationCenter подходит для широковещательных событий «один ко многим». Combine/async-stream — когда нужен поток значений с backpressure. Delegate остаётся предпочтительным, когда объект предоставляет несколько связанных методов и важна явная точка конфигурации.
Подводные камни
- Сильная ссылка на delegate — без
weakконтроллер и компонент удерживают друг друга, утечка памяти гарантирована. - Не назначить делегат до отображения — если
loginForm.delegate = selfстоит послеview.addSubviewи пользователь успевает нажать кнопку, событие теряется. - Вызов делегата не на main thread — системные API (CoreLocation, URLSession) могут вызывать делегат на фоновом потоке; без
DispatchQueue.main.asyncобновление UI приведёт к неопределённому поведению. - Протокол без AnyObject — без ограничения
: AnyObjectнельзя объявитьweak var delegate; компилятор выдаст ошибку. - Слишком толстый протокол — 15 методов в одном делегате нарушают ISP; лучше разбить на несколько узких протоколов (как UITableViewDataSource vs UITableViewDelegate).
- Обращение к delegate после dealloc — если делегат освобождён, а компонент живёт дольше, вызов через weak-ссылку безопасен (nil), но нужно убедиться, что компонент не хранит копию ссылки в другом месте.
- Путать delegate с data source — delegate реагирует на события, data source поставляет данные; смешивание обоих в одном объекте нормально для простых экранов, но усложняет тестирование.
Common mistakes
- Сводить «паттерн delegate в UIKit и почему он широко используется» к синтаксису и не объяснять иерархия view controller.
- Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии uikit-10.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «паттерн delegate в UIKit и почему он широко используется» и подтверждает ее корректным примером.
- Умеет связать ответ с main thread, тестированием и отладкой на устройстве.
- Называет ограничения подхода uikit-10, включая производительность, память и сопровождение.