Делегирование в Swift, паттерны Delegate и Data Source

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

Что такое делегирование?

Делегирование — это шаблон проектирования, который позволяет классу передавать или «делегировать» некоторые из своих обязанностей другому классу.

Представьте, что вы и я — часть команды, которая доставляет шоколадное печенье на мероприятие. Ваши обязанности заключаются в выпечке печенья, и вы делегируете на меня изготовление теста для печенье. Когда я закончу, я отдам вам тесто для печенья, чтобы вы могли его испечь.

Это не сильно отличается в программировании на Swift. Один класс делегирует другому классу часть своих обязанностей.

Пример делегирования на Swift

Во-первых, мы определяем структуру Cookie:

struct Cookie {
    var size: Int = 5
    var hasChocolateChips: Bool = false
}

Также определяем класс с именем Bakery:

class Bakery {
    func makeCookie() {
        var cookie = Cookie()
        cookie.size = 6
        cookie.hasChocolateChips = true
    }
}

У класса Bakery есть функция с makeCookie(), которая создает экземпляр структуры Cookie и устанавливает некоторые из его свойств.

На данный момент мы хотим продавать печенья тремя разными способами:

  • В булочной.
  • На сайте пекарни.
  • Оптом.

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

Во-первых, мы определяем протокол, который будет включать в себя обязанности, которые мы передаем:

protocol BakeryDelegate {
    func cookieWasBaked(_ cookie: Cookie)
}

Протокол BakeryDelegate определяет функцию cookieWasBaked(_:). Эта функция делегата будет вызываться всякий раз, когда был выпечено печенье.

Во-вторых, мы добавляем делегирование в класс Bakery:

class Bakery {
    var delegate: BakeryDelegate?
 
    func makeCookie() {
        var cookie = Cookie()
        cookie.size = 6
        cookie.hasChocolateChips = true
 
        delegate?.cookieWasBaked(cookie)
    }
}

В классе Bakery изменились две вещи:

  • Было добавлено свойство delegate.
  • Функция cookieWasBaked(_:) вызывается с помощью делегата в makeCookie().

Тип свойства delegate — это протокол, который мы определили ранее. Вы можете присвоить любое значение свойству delegate, если оно соответствует протоколу BakeryDelegate.

Свойство delegate опциональное, и мы используем опциональную цепочку при вызове функции. При этом наш код будет нормально работать, если делегат не назначен. Печенье печется, просто с ними ничего не происходит.

Давайте создадим класс делегата:

class CookieShop: BakeryDelegate {
    func cookieWasBaked(_ cookie: Cookie) {
        print("Печенье готово: \(cookie.size)")
    }
}

CookieShop принимает протокол BakeryDelegate, и соответствует этому протоколу путем реализации функции cookieWasBaked(_:).

Наконец создадим экземпляры созданных нами классов и назначим делегата:

let shop = CookieShop()
 
let bakery = Bakery()
bakery.delegate = shop
 
bakery.makeCookie()
 
// Печенье готово: 6
  • Сначала мы создаем объект CookieShop и назначаем его константе shop.
  • Затем создаем объект Bakery и назначаем его константе bakery.
  • Затем мы назначаем shop bakery.delegate. Это делает shop делегатом bakery.
  • Наконец, когда bakery печет печенье, печенье передается в магазин, который может продать его клиенту. Пекарня делегирует продажу печенья магазину.

Сила делегирования заключается в том факте, что пекарня может предоставить делегат любому классу, который принимает протокол BakeryDelegate. Пекарне не нужно знать о конкретной реализации этого протокола, а только то, что он может вызвать при необходимости функцию cookieWasBaked(_:).

Делегирование в разработке под iOS

Делегирование — один из самых распространенных шаблонов проектирования в iOS. Практически невозможно создать приложение для iOS без использования делегирования.

Большинство основных классов в iOS SDK используют делегирование:

  • Класс UITableView использует протоколы UITableViewDelegate и UITableViewDataSource для управления видом таблицы и отображением ячеек.
  • CLLocationManager использует CLLocationManagerDelegate для предоставления данных о местоположении.
  • UITextView использует UITextViewDelegate, чтобы сообщить об изменениях в текстовом поле.

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

Представьте, что вы не можете изменить код в классе Bakery так же, как вы не можете изменить код в классе CLLocationManager. Вам нужно будет запустить процесс выпечки cookie и запустить службу геолокации, а затем дождаться поступления данных. Вы получаете эти данные, используя делегирование.

Делегирование: пример с UITextView

Давайте рассмотрим пример. Вы создаете View Controller, который позволяет делать заметки. Он включает в себя текстовое поле, которое использует паттерна делегирование:

class NoteViewController: UIViewController, UITextViewDelegate {
    var textView: UITextView
 
    func viewDidLoad() {
        textView.delegate = self
    }
}

В приведенном выше коде мы определяем NoteViewController, который является подклассом UIViewController. Он принимает протокол UITextViewDelegate и настраивает текстовое поле со свойством textView.

В функции viewDidLoad(), вы присваиваете self к свойству delegate textView. Иными словами, текущий NoteViewController является делегатом текстового поля.

Согласно протоколу UITextViewDelegate, теперь мы можем реализовать ряд функций делегата для реагирования на события, происходящих в текстовом поле:

  • textViewDidBeginEditing(_:)
  • textViewDidEndEditing(_:)
  • textView(_:shouldChangeTextIn:replacementText:)
  • textViewDidChange(_:)

К примеру, когда начинается редактирование текстового поля, мы можем его выделить, чтобы показать пользователю, что происходит редактирование. Когда вызывается textViewDidChange(_:), мы можем обновить счетчик, который показывает количество символов в текстовом поле.

Делегирование и передача данных в обратном направлении

Давайте посмотрим, как передача данных от делегата будет работать для BakeryDelegate

Во-первых, мы добавляем в BakeryDelegate новую функцию:

protocol BakeryDelegate {
    func cookieWasBaked(_ cookie: Cookie)
    func preferredCookieSize() -> Int
}

Функция makeCookie() класса Bakery также меняется:

func makeCookie() {
    var cookie = Cookie()
    cookie.size = delegate?.preferredCookieSize() ?? 6
    cookie.hasChocolateChips = true
 
    delegate?.cookieWasBaked(cookie)
}

Теперь функция preferredCookieSize() дает делегату возможность выбрать размер cookie. И когда delegate равен nil, благодаря оператору оъединения по nil ?? размер будет установлен по умолчанию.

Затем мы изменяем класс делегата для использования новой функции:

class CookieShop: BakeryDelegate {
    func cookieWasBaked(_ cookie: Cookie) {
        print("Печенье готово: \(cookie.size)")
    }
 
    func preferredCookieSize() -> Int {
        return 12
    }
}

Наконец, мы запускаем тот же код, что и раньше:

let shop = CookieShop()
 
let bakery = Bakery()
bakery.delegate = shop
 
bakery.makeCookie()
// Печенье готово: 12

Мы видим, что теперь выпекается печенье с размером 12. Это связано с тем, что функция делегата preferredCookieSize() позволяет нам возвращать данные объекту Bakery.

Подобная функция preferredCookieSize() довольно распространена в iOS SDK. Например, протокол делегата табличного представления определяет функции делегата, которые настраивают размер ячеек табличного представления, верхних и нижних колонтитулов.

Еще одна частая практика в iOS SDK — это использование «did», «should» и «will» в именах функций делегата. Они часто говорят нам в какой момент вызывается функция делегата:

  • tableView(_:willSelectRowAt:) in UITableViewDelegate сообщает делегату, что мы собираемся выбрать строку табличного представления.
  • locationManager(_:didUpdateLocations:) в CLLocationManagerDelegate сообщает делегату, что пришли обновления местоположения.
  • navigationController(_:willShow:animated:) в UINavigationControllerDelegate сообщает делегату, что контроллер навигации собирается показать View Controller.

Зачем использовать делегирование?

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

Альтернативой делегированию является создание подклассов. Вместо того, чтобы использовать делегата для получения обновлений GPS-местоположения CLLocationManager, вы просто создаете подкласс и отвечаете на обновления местоположения напрямую.

Этот подход имеет огромный недостаток: вы наследуете весь класс CLLocationManager для чего-то простого, как получения данных о местоположении. Вам придется переопределить некоторые из функций по умолчанию, которые вы либо должны вызвать напрямую с помощью super, либо полностью заменить.

И наконец, создание подклассов создает тесно связанную иерархию классов, которая не имеет смысла, если ваш подкласс по своей природе не похож на класс, который вы создаете.

А как насчет шаблона Observer в NotificationCenter в качестве альтернативы делегированию? Это может сработать: вы будете реагировать на наблюдаемые изменения в каком-либо объекте.

Шаблон Observable полезен, когда ваш код должен взаимодействовать с несколькими компонентами с отношением «один ко многим» или «многие ко многим». Один компонент в вашем приложении передает сигнал, на который отвечают несколько других компонентов. И кроме передачи и связи данных, вы не можете формализовать требования для связи, как это позволяет сделать протокол.

Делегирование полезно для отношений 1-на-1, тогда как шаблон Observer больше подходит для отношений «один ко многим» или «многие ко многим».

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

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

Паттерн Delegate

Наша задача — передать данные из класса FirstViewController в класс SecondViewController с помощью делегата. Для начала создадим протокол с обязательным методом и параметром типа String.

protocol FirstViewControlleDelegate { 
func passData(data: String) 
}

Во-вторых, создадим класс с именем FirstViewController.

class FirstViewController {
 var delegate: FirstViewControlleDelegate?
}

FirstViewController имеет опциональное свойство delegate, тип которого FirstViewControlleDelegate. Свойство делегата будет инициализировано классом SecondViewController. Данный класс будет соответствовать протоколу FirstViewControlleDelegate.

class SecondViewController: FirstViewControlleDelegate {
 func passData(data: String) {
  print(data)
 }
}

Создадим два объекта и проинициализируем их.

let first = FirstViewController()
let second = SecondViewController()

Присвоим опциональное свойство delegate классу secondVC.

first.delegate = second

Далее вы можете выполнить метод passData из класса FirstViewController несмотря на то, что метод passData находится в классе SecondViewController.

first.delegate?.passData(data: "Магия!")
// "Магия!"

Можно описать отношения делегирующего класса и делегата как отношения директора и секретаря. Делегат SecondViewController является секретарем, а делегирующий класс FirstViewController директором. Директор может иметь несколько контрактов, и он может приказать секретарю обработать их.

first.delegate?.passData(data: "новые контракты")

Используем данные, переданные директором в SecondViewController.

class SecondViewController: FirstViewControlleDelegate {
 func passData(data: String) {
  print("Директор приказал мне обработать \(data)")
 }
}

Когда директор вызывает метод passData, секретарь автоматически получает задание обработать новые контракты, предоставленные директором.

first.delegate?.passData(data: "новые контракты")
// "Директор приказал мне обработать новые контракты"

Давайте посмотрим, как паттерн делегирования используется в экосистеме iOS. Если вы работали с UITableView, вы могли видеть следующий код:

class BobViewController: UIViewController, UITableViewDelegate {
 override func viewDidLoad() {
  super.viewDidLoad()
  tableView.delegate = self
 }
}

В приведенном выше коде есть два объекта. Одним из них является self, который ссылается на объект BobViewController. Второй объект относится к tableView. В этом случае директором является tableView, а секретарем — self.

Директор tableView может вызывать didSelectRowAtIndexPath, чтобы отдать секретарю свои поручения.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
 print("Директор отдал мне поручения.")
}

При этом секретарь может использовать информацию, предоставленную генеральным директором, через параметры tableView и indexPath.

Паттерн Data Source

Паттерн delegate используется для отправки данных от директора к секретарю. С паттерном data source секретарь может передавать данные директору.

Протокол содержит метод passData, но при этом возвращает значение типа String.

protocol FirstViewControlleDelegate {
 func passData(data: String) -> String
}

Для хранения данных, полученных от секретаря, директор имеет свойство dataFromSecretary.

class FirstViewController {
 var dataFromSecretary: String?
 var delegate: FirstViewControlleDelegate?
}

Секретарь общается с директором, когда передает контракты.

class SecondViewController: FirstViewControlleDelegate {
 func passData(data: String) -> String {
  print("Директар передал мне \(data)")
  return "много работы."
 }
}

Создадим экземпляры директора и секретаря, затем назначим делагата.

let first = FirstViewController() // директор
let second = SecondViewController() // секретарь
 
first.delegate = second

И вызовем метод.

first.dataFromSecretary = first.delegate?.passData(data: "много работы.") // "Директор передал мне много работы."
print(first.dataFromSecretary) // "много работы."

Когда вы видите протокол UITableViewDataSource, у него есть обязательный метод, numberOfRowsInSection. Но, подобно passData, он возвращает Int.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 return 10
}

В этом случае класс BobViewController сообщает количество строк в таблице классу tableView. Теперь tableview получает информацию и создает пользовательский интерфейс.

Читайте также:
Добавить комментарий

Ваш адрес email не будет опубликован.