Шаблон делегирования является одним из наиболее распространенных шаблонов в разработке под iOS и OS X. Это простой шаблон, который интенсивно используется инфраструктурами Apple, и даже самое простое приложение для iOS использует делегирование для выполнения своей работы. Давайте начнем с рассмотрения определения делегирования.
1. Что такое делегирование?
Определение
Определение шаблона делегирования является коротким и простым. Вот как Apple определяет шаблон.
Делегат — это объект, который действует от имени или в координации с другим объектом, когда этот объект встречает событие в программе.
Давайте разберемся с этим. Шаблон делегирования включает два объекта, делегат и делегирующий объект. Например, класс UITableView
определяет свойство delegate
которому он делегирует события. Свойство делегата должно соответствовать протоколу UITableViewDelegate
, который определен в заголовочном файле класса UITableView
.
В этом примере экземпляр табличного представления является делегирующим объектом. Делегатом обычно является контроллер представления, но это может быть любой объект, который соответствует протоколу UITableViewDelegate
. Если вы не знакомы с протоколами, класс соответствует протоколу, если он реализует необходимые методы протокола. Мы рассмотрим пример чуть позже.
Когда пользователь касается строки в табличном представлении, табличное представление уведомляет своего делегата, отправляя ему сообщение tableView(_:didSelectRowAtIndexPath:)
. Первый аргумент этого метода — табличное представление, отправляющее сообщение. Второй аргумент — индексный путь строки, которую пользователь коснулся.
Табличное представление только уведомляет своего делегата об этом событии. Именно делегат должен решить, что должно произойти, когда произошло такое событие. Такое разделение обязанностей, как вы вскоре узнаете, является одним из ключевых преимуществ модели делегирования.
преимущества
Повторное использование
Делегирование имеет несколько преимуществ, первое из которых — возможность повторного использования. Поскольку табличное представление делегирует взаимодействие с пользователем своему делегату, табличному представлению не нужно знать, что должно произойти, если постучать по одной из его строк.
Иными словами, табличное представление может оставаться в неведении о деталях реализации того, как приложение взаимодействует с пользователем. Эта ответственность делегируется делегату, например контроллеру представления.
Непосредственным преимуществом является то, что класс UITableView
можно использовать как есть в большинстве ситуаций. В большинстве случаев нет необходимости UITableView
подкласс UITableView
чтобы адаптировать его к потребностям вашего приложения.
Слабая связь
Другим важным преимуществом делегирования является слабая связь. В моей статье о синглетонах я подчеркиваю, что следует как можно больше избегать тесной связи. Делегирование — это шаблон дизайна, который активно способствует слабой связи. Что я имею в виду под этим?
Класс UITableView
связан со своим делегатом для выполнения своей работы. Если ни один делегат не связан с табличным представлением, табличное представление не может обрабатывать или реагировать на взаимодействие с пользователем. Это означает, что должен быть определенный уровень связи. Однако табличное представление и его делегат слабо связаны, поскольку каждый класс, реализующий протокол UITableViewDelegate
, может выступать в качестве делегата табличного представления. В результате получается гибкий и слабо связанный объектный граф.
Разделение обязанностей
Менее известным преимуществом делегирования является разделение обязанностей. Всякий раз, когда вы создаете граф объектов, важно знать, какие объекты отвечают за какие задачи. Структура делегирования делает это очень ясным.
В случае класса UITableView
делегат табличного представления отвечает за обработку взаимодействия с пользователем. Само табличное представление отвечает за обнаружение взаимодействия с пользователем. Это четкое разделение обязанностей. Такое разделение делает вашу работу разработчика намного проще и понятнее.
2. Пример
Есть несколько вариантов шаблона делегирования. Давайте продолжим, исследуя протокол UITableViewDelegate
.
Делегация
Протокол UITableViewDelegate
должен быть реализован делегатом табличного представления. Табличное представление уведомляет своего делегата через протокол UITableViewDelegate
о взаимодействии с пользователем, но оно также использует делегата для своего макета.
Важным отличием Swift от Objective-C является возможность пометить методы протокола как необязательные. В Objective-C методы протокола требуются по умолчанию. Однако методы протокола UITableViewDelegate
не являются обязательными. Другими словами, класс может соответствовать протоколу UITableViewDelegate
без реализации каких-либо методов протокола.
В Swift, однако, класс, соответствующий определенному протоколу, требуется для реализации каждого метода, определенного протоколом. Это намного безопаснее, поскольку делегирующему объекту не нужно проверять, реализует ли делегат метод протокола. Это тонкое, но важное различие проиллюстрировано позже в этом руководстве, когда мы реализуем шаблон делегирования.
Источник данных
Существует еще один шаблон, тесно связанный с шаблоном делегирования, шаблон источника данных . Протокол UITableViewDataSource
является примером этого шаблона. Класс UITableView
предоставляет свойство dataSource
с типом UITableViewDataSource
( id<UITableViewDataSource>
в Objective-C). Это означает, что источником данных табличного представления может быть любой объект, который реализует протокол UITableViewDataSource
.
Объект источника данных отвечает за управление источником данных объекта, источником которого он является. Важно отметить, что объект источника данных отвечает за сохранение ссылки на элементы, которые он предоставляет целевому объекту, такие как табличное представление или представление коллекции.
Например, табличное представление запрашивает у своего источника данных данные, которые необходимо отобразить. Табличное представление не несет ответственности за сохранение объектов данных, которые необходимо отобразить. Эта роль передается объекту источника данных.
Шаблон источника данных хорошо вписывается в шаблон Model-View-Controller или MVC . Это почему? Например, табличное представление является частью слоя представления. Он не знает и не должен знать о уровне модели и не отвечает за обработку данных, поступающих с уровня модели. Это подразумевает, что источником данных табличного представления или любого другого компонента представления, который реализует шаблон источника данных, часто является какой-либо контроллер. На iOS это обычно подкласс UIViewController
.
Сигнатуры методов протокола источника данных следуют той же схеме, что и сигнатуры протокола делегата. Объект, отправляющий сообщения в источник данных, передается в качестве первого аргумента. Протокол источника данных должен определять только те методы, которые относятся к данным, используемым запрашивающим объектом.
Например, табличное представление запрашивает у своего источника данных количество секций и строк, которые оно должно отображать. Но он также уведомляет источник данных, что строка или раздел были вставлены или удалены. Последнее важно, поскольку источник данных должен обновляться сам, чтобы отразить изменения, видимые в табличном представлении. Если представление таблицы и источник данных не синхронизированы, могут произойти неприятности.
3. Реализация
Objective-C
Реализация шаблона делегата довольно проста теперь, когда мы понимаем, как он работает. Взгляните на следующий пример Objective-C.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
#import <UIKit/UIKit.h>
@protocol AddItemViewControllerDelegate;
@interface AddItemViewController : UIViewController
@property (weak, nonatomic) id<AddItemViewControllerDelegate> delegate;
@end
@protocol AddItemViewControllerDelegate <NSObject>
— (void)viewControllerDidCancel:(AddItemViewController *)viewController;
— (void)viewController:(AddItemViewController *)viewController didAddItem:(NSString *)item;
@optional
— (BOOL)viewController:(AddItemViewController *)viewController validateItem:(NSString *)item;
@end
|
Мы объявляем класс AddItemViewController
, который расширяет UIViewController
. Класс объявляет свойство delegate
с id<AddItemViewControllerDelegate>
типа id<AddItemViewControllerDelegate>
. Обратите внимание, что свойство помечено как слабое , что означает, что экземпляр AddItemViewController
сохраняет слабую ссылку на свой делегат.
Также обратите внимание, что я добавил декларацию прямого протокола ниже оператора import инфраструктуры UIKit. Это необходимо, чтобы избежать предупреждения компилятора. Мы могли бы переместить объявление протокола ниже оператора импорта, но я предпочитаю размещать его ниже интерфейса класса. Это не более чем личное предпочтение.
Декларация протокола также довольно проста. Протокол AddItemViewControllerDelegate
расширяет протокол AddItemViewControllerDelegate
. Это не обязательно, но это окажется очень полезным. Мы узнаем, почему это немного позже.
Протокол AddItemViewControllerDelegate
объявляет два обязательных метода и один необязательный метод. Как я упоминал ранее, хорошей практикой является передача делегирующего объекта в качестве первого параметра каждого метода делегата, чтобы сообщить делегату, какой объект отправляет сообщение.
Обязательные методы уведомляют делегата о событии, отмене или добавлении. Необязательный метод запрашивает обратную связь у делегата. Ожидается, что делегат вернет YES
или NO
.
Это первая часть головоломки делегации. Мы объявили класс, который объявляет свойство delegate
и мы объявили протокол делегата. Вторая часть головоломки — AddItemViewController
методов делегата в классе AddItemViewController
. Посмотрим, как это работает.
В реализации класса AddItemViewController
мы реализуем действие cancel:
Это действие может быть связано с кнопкой в пользовательском интерфейсе. Если пользователь касается кнопки, делегат уведомляется об этом событии, и в результате делегат может отклонить экземпляр AddItemViewController
.
1
2
3
4
5
|
— (IBAction)cancel:(id)sender {
if (self.delegate && [self.delegate respondsToSelector:@selector(viewControllerDidCancel:)]) {
[self.delegate viewControllerDidCancel:self];
}
}
|
Рекомендуется убедиться, что объект делегата не равен nil
и что он реализует метод делегата, который мы собираемся вызвать, viewControllerDidCancel:
Это легко благодаря методу respondsToSelector:
объявленному в протоколе NSObject
. По этой причине протокол AddItemViewControllerDelegate
расширяет протокол AddItemViewControllerDelegate
. Расширяя протокол NSObject
, мы получаем эту функциональность бесплатно.
Вы можете опустить проверку на то, что свойство делегата равно nil
, так как respondsToSelector:
вернет nil
если свойство делегата равно nil
. Я обычно добавляю эту проверку, поскольку она четко показывает, что мы тестируем.
Третий и последний кусок головоломки — реализация протокола делегата объектом делегата. В следующем фрагменте кода показано создание экземпляра AddItemViewController
и реализация одного из методов делегата.
01
02
03
04
05
06
07
08
09
10
|
— (IBAction)addItem:(id)sender {
// Initialize View Controller
AddItemViewController *viewController = [[AddItemViewController alloc] init];
// Configure View Controller
[viewController setDelegate:self];
// Present View Controller
[self presentViewController:viewController animated:YES completion:nil];
}
|
1
2
3
4
|
— (void)viewControllerDidCancel:(AddItemViewController *)viewController {
// Dismiss Add Item View Controller
…
}
|
Не забудьте согласовать класс, который действует как делегат, с протоколом AddItemViewControllerDelegate
, как показано ниже. Вы можете добавить это в интерфейс класса или в расширение частного класса.
1
2
3
4
5
|
#import «AddItemViewController.h»
@interface ViewController () <AddItemViewControllerDelegate>
@end
|
стриж
В Swift шаблон делегирования также прост в реализации, и вы обнаружите, что Swift делает делегирование немного более элегантным. Давайте реализуем приведенный выше пример в Swift. Так выглядит класс AddItemViewController
в Swift.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
import UIKit
protocol AddItemViewControllerDelegate: NSObjectProtocol {
func viewControllerDidCancel(viewController: AddItemViewController)
func viewController(viewController: AddItemViewController, didAddItem: String)
func viewController(viewController: AddItemViewController, validateItem: String) -> Bool
}
class AddItemViewController: UIViewController {
var delegate: AddItemViewControllerDelegate?
func cancel(sender: AnyObject) {
delegate?.viewControllerDidCancel(self)
}
}
|
Объявление протокола выглядит немного иначе в Swift. Обратите внимание, что протокол AddItemViewControllerDelegate
расширяет протокол AddItemViewControllerDelegate
протокола AddItemViewControllerDelegate
. В Swift классы и протоколы не могут иметь одно и то же имя, поэтому протокол NSObject
называется по-разному в Swift.
Свойство delegate
является переменной типа AddItemViewControllerDelegate?
, Обратите внимание на вопросительный знак в конце названия протокола. Свойство делегата является необязательным.
В методе cancel(_:)
мы вызываем делегированный метод viewControllerDidCancel(_:)
. Эта строка показывает, насколько элегантным может быть Swift. Мы благополучно разворачиваем свойство delegate
перед вызовом метода делегата. Нет необходимости проверять, реализует ли делегат метод viewControllerDidCancel(_:)
поскольку в Swift требуется каждый метод протокола.
Давайте теперь посмотрим на класс ViewController
, который реализует протокол AddItemViewControllerDelegate
. Интерфейс показывает нам, что класс ViewController
расширяет класс UIViewController
и принимает протокол AddItemViewControllerDelegate
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
import UIKit
class ViewController: UIViewController, AddItemViewControllerDelegate {
func addItem(send: AnyObject) {
// Initialize View Controller
let viewController = AddItemViewController()
// Configure View Controller
viewController.delegate = self
// Present View Controller
presentViewController(viewController, animated: true, completion: nil)
}
func viewControllerDidCancel(viewController: AddItemViewController) {
// Dismiss Add Item View Controller
…
}
func viewController(viewController: AddItemViewController, didAddItem: String) {
}
func viewController(viewController: AddItemViewController, validateItem: String) -> Bool {
}
}
|
В addItem(_:)
мы инициализируем экземпляр класса AddItemViewController
, устанавливаем его свойство delegate
и представляем его пользователю. Обратите внимание, что мы реализовали каждый метод AddItemViewControllerDelegate
протокола AddItemViewControllerDelegate
. Если мы этого не сделаем, компилятор скажет нам, что класс ViewController
не соответствует протоколу AddItemViewControllerDelegate
. Попробуйте это, закомментировав один из методов делегата.
Вывод
Делегирование — это шаблон, с которым вы часто сталкиваетесь при разработке приложений для iOS и OS X. Какао сильно зависит от этого шаблона проектирования, поэтому важно ознакомиться с ним.
С момента появления блоков несколько лет назад Apple постепенно предлагала альтернативный API на основе блоков для некоторых реализаций делегирования. Некоторые разработчики последовали примеру Apple, предлагая свои собственные альтернативы на основе блоков. Например, популярная библиотека AFNetworking в значительной степени опирается на блоки, а не на делегирование, что приводит к элегантному, интуитивно понятному API.