Delegate — це патерн проєктування, за якого один об'єкт делегує виконання завдання іншому об'єкту. В iOS патерн реалізується через протоколи Swift та @protocol Objective-C. Делегування — один із фундаментальних патернів Cocoa Touch, що використовується в UITableViewDelegate, UITextFieldDelegate та сотнях інших API Apple. За даними Apple Developer Documentation (2025), близько 70% системних класів UIKit використовують делегати для кастомізації поведінки без успадкування.
Головне
Delegate — це патерн поведінкового проєктування, що дозволяє об'єкту делегувати частину своїх обов'язків іншому об'єкту. На відміну від успадкування, де дочірній клас перевизначає методи батька, делегат використовує композицію: об'єкт-власник зберігає посилання на делегата та викликає його методи у визначених точках.
Делегат визначає протокол — набір методів, які делегат може реалізувати. Методи поділяються на обов'язкові (required) та опціональні (optional). В Swift опціональні методи протоколу позначаються ключовим словом @objc optional.
Об'єкт A (власник) містить властивість delegate — weak-посилання на об'єкт B (делегат). Коли відбувається подія, A перевіряє, чи реалізує B відповідний метод протоколу, і викликає його. Weak-посилання обов'язкове: без нього делегат не зможе звільнитися з пам'яті, оскільки власник утримує його сильним посиланням.
protocol LoaderDelegate: AnyObject {
func loaderDidStart(_ loader: DataLoader)
func loader(_ loader: DataLoader, didLoad data: Data)
func loader(_ loader: DataLoader, didFailWith error: Error)
}
class DataLoader {
weak var delegate: LoaderDelegate?
func start() {
delegate?.loaderDidStart(self)
// асинхронне завантаження
}
}Клас DataLoader визначає протокол LoaderDelegate та викликає методи делегата в ключових точках життєвого циклу завантаження. AnyObject гарантує, що протокол може бути реалізований лише класами — це необхідно для weak-посилання.
Реалізація делегата в Swift включає три кроки: оголошення протоколу, створення weak-властивості delegate у власнику та реалізація протоколу в класі-делегаті. Розглянемо на прикладі кастомного UITextField з валідацією.
protocol ValidatorDelegate: AnyObject {
func validate(_ input: String) -> Bool
func validatorDidFail(_ input: String)
}
class ValidatedTextField: UITextField {
weak var validator: ValidatorDelegate?
override func textDidChange() {
guard let text = self.text else { return }
if validator?.validate(text) == false {
validator?.validatorDidFail(text)
self.layer.borderColor = UIColor.red.cgColor
}
}
}
class LoginViewController: UIViewController, ValidatorDelegate {
let textField = ValidatedTextField()
override func viewDidLoad() {
super.viewDidLoad()
textField.validator = self
}
func validate(_ input: String) -> Bool {
return input.count >= 6
}
func validatorDidFail(_ input: String) {
print("Валідацію не пройдено: занадто коротке введення")
}
}LoginViewController реалізує протокол ValidatorDelegate та призначає себе делегатом textField. При кожній зміні тексту ValidatedTextField викликає validate(_:), а якщо валідація не пройдена — validatorDidFail(_:). Controller виступає посередником між View та логікою валідації.
Вибір між делегатом, сповіщеннями та замиканнями залежить від кількості отримувачів та зв'язаності компонентів. Кожен механізм вирішує задачу комунікації між об'єктами, але з різними компромісами.
| Характеристика | Delegate | NotificationCenter | Closure |
|---|---|---|---|
| Тип зв'язку | 1:1 | 1:N | 1:1 |
| Зв'язаність | Слабка (через протокол) | Дуже слабка (рядок-ключ) | Середня (захоплення контексту) |
| Типобезпека | Повна | Відсутня (Any?) | Повна |
| Ризик retain cycle | Немає (weak) | Немає | Є (захоплення self) |
| Коли застосовувати | Складні колбеки з кількома методами | Події, що цікавлять багатьох | Прості замикання з 1-2 колбеками |
Delegate оптимальний, коли потрібно передати серію пов'язаних подій одному отримувачу. NotificationCenter кращий для broadcast-сповіщень. Closure — для простих асинхронних операцій, таких як completion handler в URLSession.
Objective-C використовує @protocol та @optional для оголошення делегатів. На відміну від Swift, всі методи протоколу за замовчуванням опціональні. Ключова відмінність — виклик respondsToSelector: перед відправкою повідомлення делегату, оскільки метод може бути не реалізований.
@protocol ImageCacheDelegate
@optional
- (void)cacheDidStartDownload: (ImageCache *)cache;
- (void)cache: (ImageCache *)cache didCacheImage: (UIImage *)image;
@required
- (void)cache: (ImageCache *)cache didFailWithError: (NSError *)error;
@end
@interface ImageCache : NSObject
@property (nonatomic, weak) id<ImageCacheDelegate> delegate;
- (void)downloadImageAtURL: (NSURL *)url;
@end
@implementation ImageCache
- (void)downloadImageAtURL: (NSURL *)url {
if ([self.delegate respondsToSelector:@selector(cacheDidStartDownload:)]) {
[self.delegate cacheDidStartDownload:self];
}
// асинхронне завантаження зображення
}
@endКлючова відмінність Objective-C: перед викликом опціонального методу необхідна перевірка respondsToSelector:. В Swift опціональні методи протоколу виключають цю перевірку — optional chaining (?.) автоматично обробляє випадок відсутності реалізації.
Помилки використання делегатів призводять до витоків пам'яті, падіння додатку та неочевидних багів. Розглянемо п'ять найпоширеніших проблем.
Retain cycle — найпоширеніша помилка. Якщо властивість delegate оголошено як strong, а делегат своєю чергою володіє об'єктом-власником, утворюється цикл утримання. Обидва об'єкти ніколи не будуть звільнені з пам'яті. Рішення: завжди оголошувати делегата як weak var в Swift або @property (weak) в Objective-C.
Якщо об'єкт-власник переживає делегата, а посилання залишилося, виклик методу делегата призведе до EXC_BAD_ACCESS. Weak-посилання вирішує цю проблему автоматично: після звільнення делегата властивість стає nil. Однак у багатопотокових сценаріях варто додатково перевіряти делегата в головному потоці.
Протокол з 20+ методами порушує принцип розділення інтерфейсів (ISP). UITableViewDelegate містить близько 30 опціональних методів — це історично сформований виняток. У власних протоколах краще розділити відповідальність на кілька дрібних протоколів, кожен зі своєю роллю.
Системні API Apple активно використовують патерн Delegate. Розглянемо три ключові приклади з UIKit, які зустрічаються в кожному iOS-додатку.
| API | Протокол | Ключові методи |
|---|---|---|
| UITableView | UITableViewDelegate | didSelectRowAt, heightForRowAt, willDisplay |
| UITextField | UITextFieldDelegate | shouldChangeCharactersIn, didBeginEditing, shouldReturn |
| URLSession | URLSessionDelegate | didReceiveChallenge, didCompleteWithError, didBecomeInvalidWithError |
Кожен із цих протоколів реалізує різні аспекти поведінки: UITableViewDelegate керує зовнішнім виглядом та реакцією на дотики, UITextFieldDelegate контролює введення тексту, URLSessionDelegate обробляє мережеві події. Це демонструє гнучкість патерну: делегат може бути адаптований під будь-яку область відповідальності.
Часті запитання
Delegate керує поведінкою та зовнішнім виглядом (висота комірки, реакція на натискання). DataSource надає дані (кількість рядків, вміст комірок). В UITableViewDelegate та UITableViewDataSource — це два окремі протоколи, що розділяють відповідальність за представлення та дані.
Weak-посилання запобігає retain cycle. Власник (наприклад, UITableView) утримує посилання на делегата лише як weak. Якщо делегат (UIViewController) володіє таблицею, strong-посилання на делегата створило б цикл: ViewController → UITableView → Delegate (ViewController). Weak розриває цей цикл.
В SwiftUI патерн Delegate використовується рідше — його замінюють @Binding, @State та замикання. Однак delegate все ще застосовується для інтеграції UIKit через UIViewRepresentable. Наприклад, MKMapViewDelegate та WKUIDelegate залишаються актуальними при обгортанні UIKit-компонентів у SwiftUI.
@objc optional дозволяє оголосити необов'язкові методи в протоколі Swift. Це механізм сумісності з Objective-C runtime. Без @objc всі методи протоколу Swift за замовчуванням обов'язкові. Optional використовується в UIKit-протоколах, де делегат може реалізувати лише потрібні йому методи.
В одного об'єкта може бути лише один делегат для кожної властивості delegate. Якщо потрібно сповістити кілька об'єктів, використовуйте multicast delegate, масив делегатів або NotificationCenter. Патерн Delegate спочатку спроєктований як відношення 1:1.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також