Статьи

Шаблоны проектирования: делегирование

Шаблон делегирования является одним из наиболее распространенных шаблонов в разработке под iOS и OS X. Это простой шаблон, который интенсивно используется инфраструктурами Apple, и даже самое простое приложение для iOS использует делегирование для выполнения своей работы. Давайте начнем с рассмотрения определения делегирования.

Определение шаблона делегирования является коротким и простым. Вот как Apple определяет шаблон.

Делегат — это объект, который действует от имени или в координации с другим объектом, когда этот объект встречает событие в программе.

Давайте разберемся с этим. Шаблон делегирования включает два объекта, делегат и делегирующий объект. Например, класс UITableView определяет свойство delegate которому он делегирует события. Свойство делегата должно соответствовать протоколу UITableViewDelegate , который определен в заголовочном файле класса UITableView .

В этом примере экземпляр табличного представления является делегирующим объектом. Делегатом обычно является контроллер представления, но это может быть любой объект, который соответствует протоколу UITableViewDelegate . Если вы не знакомы с протоколами, класс соответствует протоколу, если он реализует необходимые методы протокола. Мы рассмотрим пример чуть позже.

Когда пользователь касается строки в табличном представлении, табличное представление уведомляет своего делегата, отправляя ему сообщение tableView(_:didSelectRowAtIndexPath:) . Первый аргумент этого метода — табличное представление, отправляющее сообщение. Второй аргумент — индексный путь строки, которую пользователь коснулся.

Табличное представление только уведомляет своего делегата об этом событии. Именно делегат должен решить, что должно произойти, когда произошло такое событие. Такое разделение обязанностей, как вы вскоре узнаете, является одним из ключевых преимуществ модели делегирования.

Делегирование имеет несколько преимуществ, первое из которых — возможность повторного использования. Поскольку табличное представление делегирует взаимодействие с пользователем своему делегату, табличному представлению не нужно знать, что должно произойти, если постучать по одной из его строк.

Иными словами, табличное представление может оставаться в неведении о деталях реализации того, как приложение взаимодействует с пользователем. Эта ответственность делегируется делегату, например контроллеру представления.

Непосредственным преимуществом является то, что класс UITableView можно использовать как есть в большинстве ситуаций. В большинстве случаев нет необходимости UITableView подкласс UITableView чтобы адаптировать его к потребностям вашего приложения.

Другим важным преимуществом делегирования является слабая связь. В моей статье о синглетонах я подчеркиваю, что следует как можно больше избегать тесной связи. Делегирование — это шаблон дизайна, который активно способствует слабой связи. Что я имею в виду под этим?

Класс UITableView связан со своим делегатом для выполнения своей работы. Если ни один делегат не связан с табличным представлением, табличное представление не может обрабатывать или реагировать на взаимодействие с пользователем. Это означает, что должен быть определенный уровень связи. Однако табличное представление и его делегат слабо связаны, поскольку каждый класс, реализующий протокол UITableViewDelegate , может выступать в качестве делегата табличного представления. В результате получается гибкий и слабо связанный объектный граф.

Менее известным преимуществом делегирования является разделение обязанностей. Всякий раз, когда вы создаете граф объектов, важно знать, какие объекты отвечают за какие задачи. Структура делегирования делает это очень ясным.

В случае класса UITableView делегат табличного представления отвечает за обработку взаимодействия с пользователем. Само табличное представление отвечает за обнаружение взаимодействия с пользователем. Это четкое разделение обязанностей. Такое разделение делает вашу работу разработчика намного проще и понятнее.

Есть несколько вариантов шаблона делегирования. Давайте продолжим, исследуя протокол 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 .

Сигнатуры методов протокола источника данных следуют той же схеме, что и сигнатуры протокола делегата. Объект, отправляющий сообщения в источник данных, передается в качестве первого аргумента. Протокол источника данных должен определять только те методы, которые относятся к данным, используемым запрашивающим объектом.

Например, табличное представление запрашивает у своего источника данных количество секций и строк, которые оно должно отображать. Но он также уведомляет источник данных, что строка или раздел были вставлены или удалены. Последнее важно, поскольку источник данных должен обновляться сам, чтобы отразить изменения, видимые в табличном представлении. Если представление таблицы и источник данных не синхронизированы, могут произойти неприятности.

Реализация шаблона делегата довольно проста теперь, когда мы понимаем, как он работает. Взгляните на следующий пример 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 . Попробуйте это, закомментировав один из методов делегата.

Предупреждение о реализации протокола Swift

Делегирование — это шаблон, с которым вы часто сталкиваетесь при разработке приложений для iOS и OS X. Какао сильно зависит от этого шаблона проектирования, поэтому важно ознакомиться с ним.

С момента появления блоков несколько лет назад Apple постепенно предлагала альтернативный API на основе блоков для некоторых реализаций делегирования. Некоторые разработчики последовали примеру Apple, предлагая свои собственные альтернативы на основе блоков. Например, популярная библиотека AFNetworking в значительной степени опирается на блоки, а не на делегирование, что приводит к элегантному, интуитивно понятному API.