14 июля

Делегаты и колбэки в Swift простым языком. Что же такое этот delegate, и как работает callback

ПрограммированиеРазработка под iOSРазработка мобильных приложенийSwiftИзучение языков
Tutorial
Из песочницы

В Swift при изучении UI (User Interface) каждый рано или поздно приходит к необходимости использования делегата. Все гайды о них пишут, и вроде бы делаешь, как там написано, и вроде бы работает, но почему и как это работает, не у каждого в голове укладывается до конца. Лично у меня даже какое-то время складывалось ощущение, что delegate – это какое-то волшебное слово, и что оно прям встроено в язык программирования (вот, насколько запутаны были мои мысли от этих гайдов). Давайте попытаемся объяснить простым языком, что же это такое. А разобравшись с делегатом, уже гораздо легче будет понять, что такое колбэк (callback), и как работает он.


Официант и повар


Итак, перед тем как перейти к коду давайте представим себе некоего официанта и какого-нибудь повара. Официант получил заказ от клиента за столиком, но сам он готовить не умеет, и ему нужно попросить об этом повара. Он может пойти на кухню и сказать повару: «Приготовь курицу». У повара есть соответствующие инструменты (сковорода, масло, огонь…) и навык приготовления. Повар готовит и отдает блюдо официанту. Официант берет то, что сделано поваром и несет к клиенту.


А теперь представим себе ситуацию, что официант не может прибежать на кухню и сказать повару напрямую, какое блюдо у него заказали. Не пускают его на кухню (допустим, такие правила) или кухня находится на другом этаже (устанешь бегать). И единственный способ общения – это окошко мини-лифта. Официант кладет туда записку, нажимает кнопку, лифт уехал на кухню. Приезжает обратно с готовым блюдом. Запомнили? Теперь зафиксируем ситуацию в голове, попробуем воссоздать через код и понять, как это связано с нашей темой.


Перенесем в код


Создаем классы официанта и повара. Для простоты сделаем это в плейграунде:


import UIKit

// Создаем класс официанта
class Waiter {

    /// Свойство "заказ" - опциональная информация о текущем заказе. О заказе может узнать только официант, поэтому "private".
    private var order: String?

    /// Метод "принять заказ".
    func takeOrder(_ food: String) {
        print("What would you like?")
        print("Yes, of course!")
        order = food
        sendOrderToCook()
    }

    /// Метод "отправить заказ повару". Мог бы сделать только официант. Но как?
    private func sendOrderToCook() {
        // ??? Как передать повару заказ?
    }

    /// Метод "доставить блюдо клиенту". Умеет только официант.
    private func serveFood() {
        print("Your \(order!). Enjoy your meal!")
    }

}

// Создаем класс повара
class Cook {

    /// Свойство "сковорода". Есть только у повара.
    private let pan: Int = 1

    /// Свойство "плита". Есть только у повара.
    private let stove: Int = 1

    /// Метод "приготовить". Умеет только повар.
    private func cookFood(_ food: String) -> Bool {
        print("Let's take a pan")
        print("Let's put \(food) on the pan")
        print("Let's put the pan on the stove")
        print("Wait a few minutes")
        print("\(food) is ready!")
        return true
    }

}

Теперь создаем их экземпляры (нанимаем на работу), и просим официанта получить заказ (курицу):


// Нанимаем на работу официанта и повара (создаем экземпляры):
let waiter = Waiter()
let cook = Cook()

// Сначала скажем официанту получить заказ. Допустим, он получает курицу:
waiter.takeOrder("Chiken")

Как теперь официанту передать повару, что ему приготовить?


Конечно, можно было бы написать ниже вот так, если бы свойства и методы официанта и повара не были private. Но так как у них уровень доступа private, то это не сработает:


cook.cookFood(waiter.order!)
// 'cookFood' is inaccessible due to 'private' protection level
// 'order' is inaccessible due to 'private' protection level

И к тому же мы использовали некий сторонний код «снаружи» классов официанта и повара, которому необходимо иметь доступ к private свойствам и методам этих классов. А как официанту передать "изнутри" себя, используя встроенные свойства и методы своего класса? Тот же вопрос со стороны повара: "Как ему приготовить то, что известно только официанту, используя свойства и методы своего класса?"


Тут на помощь приходит "лифт". В этот лифт официант кладет записку с заказом. А повар берет записку из лифта и ставит в лифт готовое блюдо для передачи повару. Такой "лифт" реализуется через протокол "Взаимообмен через лифт":


protocol InterchangeViaElevatorProtocol {
    func cookOrder(order: String) -> Bool
}

В данном случае мы говорим, что у лифта есть "интерфейс", который должен быть понятен и официанту, и повару. То есть правила взаимообмена через этот лифт. Правила взаимообмена должны знать и официант, и повар. Они простые: официант кладет записку, а повар готовит по ней.



Теперь давайте поправим классы, чтобы официант мог общаться с поваром через этот лифт. Научим их работать по этому правилу(протоколу).


Подпишем класс повара под протокол лифта. Грубо говоря, мы научим всех наших поваров соблюдать правила, описанные в этом протоколе "Обмен через лифт". В таком случае Xcode заставит нас дописать в класс повара метод из протокола. Этот метод в данном примере должен будет вернуть Bool значение.


В этом методе мы вызовем ранее созданный метод cookFood, который повар умеет выполнять.


extension Cook: InterchangeViaElevatorProtocol {
    func cookOrder(order: String) -> Bool {
        cookFood(order)
    }
}

Далее официанту добавим свойство "получатель заказа через лифт". Официант знает, что этот получатель знает правила и приготовит то, что в записке.


extension Waiter {
    var receiverOfOrderViaElevator: InterchangeViaElevatorProtocol? { return cook }
}

В данном примере мы использовали расширение, которое не позволяет делать переменные с хранимыми свойствами. Поэтому мы вынуждены дописать в фигурных скобках return cook.


Но можно сделать по-другому: просто добавить без расширения прямо внутрь класса официанта это опциональное свойство, а затем извне этому свойству присвоить экземпляр нашего повара. Главное не забыть подписать класс повара под протокол.


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


Вот теперь как будут выглядеть наши классы:


import UIKit

protocol InterchangeViaElevatorProtocol {
    func cookOrder(order: String) -> Bool
}

class Waiter {

    // Далее официанту добаим свойство "получатель заказа через лифт". Официанту известно, что этот получатель знает правила и приготовит то, что в записке.
    var receiverOfOrderViaElevator: InterchangeViaElevatorProtocol?

    var order: String?

    func takeOrder(_ food: String) {
        print("What would you like?")
        print("Yes, of course!")
        order = food
        sendOrderToCook()
    }

    private func sendOrderToCook() {
        // ??? Как передать повару заказ?
    }

    private func serveFood() {
        print("Your \(order!). Enjoy your meal!")
    }

}

// Создаем класс повара
class Cook: InterchangeViaElevatorProtocol {

    private let pan: Int = 1
    private let stove: Int = 1

    private func cookFood(_ food: String) -> Bool {
        print("Let's take a pan")
        print("Let's put \(food) on the pan")
        print("Let's put the pan on the stove")
        print("Wait a few minutes")
        print("\(food) is ready!")
        return true
    }

    // Необходимый метод, согласно правилу(протоколу):
    func cookOrder(order: String) -> Bool {
        cookFood(order)
    }

}

В классе официанта убрали пока private у свойства order (нужно сейчас для наглядности).


Далее все по той же схеме:


  1. Нанимаем официанта и повара
  2. Добавим официанту заказ:

// Нанимаем на работу официанта и повара:
let waiter = Waiter()
let cook = Cook()

// Добавим официанту заказ:
waiter.takeOrder("Chiken")

Теперь скажем официанту, что его "получатель заказа через лифт" – это наш повар.


// Теперь скажем официанту, что его "получатель заказа через лифт" - это наш повар:
waiter.receiverOfOrderViaElevator = cook

Как уже говорилось ранее, официант знает, что этот получатель знает правила и приготовит то, что в записке.


Теперь официант может нашего "получателя заказа через лифт" попросить приготовить заказ:


// Теперь официант может нашего "получателя заказа через лифт" попросить приготовить заказ:
waiter.receiverOfOrderViaElevator?.cookOrder(order: waiter.order!)

Запускаем код, получаем результат!


/*
 What would you like?
 Yes, of course!
 Let's take a pan
 Let's put Chiken on the pan
 Let's put the pan on the stove
 Wait a few minutes
 Chiken is ready!
 */

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


Можно теперь доделать у класса «Официант» метод передачи заказа, чтобы официант мог делать это «изнутри себя», то есть используя свои собственные свойства и методы, а не указания со стороны.


    private func sendOrderToCook() {
        //Добавим вызов метода cookOrder у нашего "получателя заказов через лифт":
        receiverOfOrderViaElevator?.cookOrder(order: order!)
    }

Вот и все дела! В данном случае мы добавляли официанту опциональное свойство receiverOfOrderViaElevator, подписанное под протокол. Это свойство и есть делегат. Можете заменить название этого свойства на delegate, если хотите. По сути ничего не изменится, просто это более универсальное слово, вот и все.


Теперь вы понимаете принцип работы делегата? «Окей, – скажете вы. – А как это использовать в UI?»


Как использовать delegate при создании контроллеров в UI?


В UI с необходимостью использования делегата чаще всего встречаются в случае, когда необходимо от одного «дочернего» контроллера передать информацию «родительскому». Например, нужно передать от ячейки информацию создавшему ее table view или collection view. От table view или collection view передать информацию ячейке легко: это можно сделать при ее инициализации. А вот обратно передать информацию сама ячейка напрямую не может. Вот тут и спасает шаблон (паттерн) под названием «Делегат» («Delegate»).


Кстати, слово Delegable в русской раскладке будет как «Вудупфиду». Так вот, значит, о чем пела Мерлин Монро!


Давайте представим, что повар – это хозяин кафе. И он нанимает нашего официанта. То есть создает экземпляр класса Waiter. Добавим ему умение(метод) hireWaiter. Получим вот такой класс (кстати, пусть на этот раз это будет шеф-повар):


// Создаем класс шеф-повара
class Chief: InterchangeViaElevatorProtocol {

    private let pan: Int = 1
    private let stove: Int = 1

    private func cookFood(_ food: String) -> Bool {
        print("Let's take a pan")
        print("Let's put \(food) on the pan")
        print("Let's put the pan on the stove")
        print("Wait a few minutes")
        print("\(food) is ready!")
        return true
    }

    // Необходимый метод, согласно правилу(протоколу):
    func cookOrder(order: String) -> Bool {
        cookFood(order)
    }

    // Шеф-повар умеет нанимать официантов в свое кафе:
    func hireWaiter() -> Waiter {
        return Waiter()
    }

}

Теперь шеф-повар открывает кафе и нанимает официанта (создаем экземпляр шеф-повара и вызываем у него метод hireWaiter):


// Создаем экземпляр шеф-повара (шеф-повар открывает кафе):
let chief = Chief()

// Шев-повар нанимает официанта:
let waiter = chief.hireWaiter()

// Добавим официанту заказ:
waiter.takeOrder("Chiken")

Далее все по старинке. Нужно обучить официанта, что его "получатель заказа через лифт" – это наш шеф-повар. И тогда он сможет передать ему заказ.


// Обучаем официанта, что его "получатель заказа через лифт" - это наш шеф-повар:
waiter.receiverOfOrderViaElevator = chief
// Теперь официант может нашего "получателя заказа через лифт" попросить приготовить заказ:
waiter.receiverOfOrderViaElevator?.cookOrder(order: waiter.order!)

Если запустить код, то снова все сработает.


Отлично. А теперь представим, что появился новый шеф-повар, который рассказывает официанту ещё при найме на работу, что его «получателем заказа через лифт» будет сам шеф-повар.


class SmartChief: Chief {

    override func hireWaiter() -> Waiter {
        let waiter = Waiter()
        waiter.receiverOfOrderViaElevator = self // Сразу же настраивает официанту свойство получателя заказа через лифт
        return waiter
    }

}

Здесь мы просто наследовали класс SmartChief от класса Chief и переписали метод найма официантов.


Получается, что теперь нет необходимости как-то отдельно указывать официанту (обучать), кто его получатель заказа через лифт. Он с самого начала работы уже об этом знает!


let smartChief = SmartChief()
let smartWaiter = smartChief.hireWaiter()
smartWaiter.takeOrder("Fish")
/*
 What would you like?
 Yes, of course we have Fish!
 Let's take a pan
 Let's put Fish on the pan
 Let's put the pan on the stove
 Wait a few minutes
 Fish is ready!
 */

Тоже самое и в контроллерах:


  1. Пишем протокол (правило), в котором описываем нужный нам метод, который принимает необходимые аргументы и, если нужно, что-то выдает.
  2. Подписываем «порождающий» контроллер под этот протокол и пишем в нем этот метод.
  3. У дочернего контроллера (например, у ячейки) добавляем опциональное свойство делегата, являющееся типом нашего протокола (то есть поддерживающего тип нашего протокола)
  4. При создании дочернего контроллера ставим в его свойстве делегата себя, то есть self «порождающего» контроллера.

Естественно, это можно делать не только с «порождающими» контроллерами, а вообще с любыми объектами, которые мы хотим научить общаться друг с другом.


На этом про делегаты всё. Надеюсь, было полезно! Теперь разберемся в колбэках.


Колбэки (колбеки, callback). Это тот же делегат? Ну, почти


Итак, мы выяснили, что делегаты позволяют одним объектам «общаться» с другими. Иначе говоря, позволяют одним объектам добиваться своей цели с помощью свойств и методов других объектов. Для чего же нужны колбэки? И почему они в одной статье с делегатами?


Колбэк (callback) – это тоже шаблон программирования, как и делегат. С его помощью также одни объекты могут добиваться своих целей с помощью свойств и методов других объектов. Только делать они это будут сами.

Ленивый шеф-повар, талантливый официант


Представьте себе, что в другом ресторане шеф-повар очень ленивый (или усталый, или заболел). Официант принял заказ, звонит повару, а тот ему говорит: «Давай-ка сам приготовь! Возьми мою сковородку, поставь на плиту и приготовь по рецепту!» Наш официант оказался не из робкого десятка, берет инструкцию, инструменты повара и все делает сам. Это и есть подход через колбэк.


Давайте посмотрим это на практике.


Создадим класс талантливого официанта. Добавим опциональное свойство функционального типа. Под этим названием кроется функция, которая принимает на вход аргумент с типом String и возвращает результат с типом Bool. Прямо как метод cookFood у нашего повара! Это что-то вроде способности сделать что-то по инструкции.


/// Класс талантливого официанта
class TalentedWaiter {

    var order: String?

    // Добавим опциональное свойство функционального  типа. Это функция, которая принимает на вход аргумент с типом String и возвращает результат с типом Bool.
    var doEverything: ((String) -> Bool)?

    func takeOrder(_ food: String) {
        print("What would you like?")
        print("Yes, of course we have \(food)!")
        order = food
        // Вместо передачи заказа шев-повару официант попытается сделать сам:
        doOwnself()
    }

    private func doOwnself() -> Bool {
        // Если инструкция существует, то он ее выполнит:
        if let doEverything = doEverything {
            let doOwnself = doEverything(order!)
            return doOwnself
        } else {
            return false
        }
    }

}

Далее создаем класс ленивого шеф-повара. В этот раз при найме на работу талантливого официанта, повар не назначает себя делегатом, а обучает его самому готовить. Он передает в именованное функциональное свойство официанта свое умение готовить, которое включает в себя и доступ к сковородке с плитой. То есть он присваивает замыкание его свойству, и в этом замыкании передает свой собственный метод:


// Создаем класс ленивого шеф-повара
class LazyChief {

    private let pan: Int = 1
    private let stove: Int = 1

    private func cookFood(_ food: String) -> Bool {
        print("I have \(pan) pan")
        print("Let's put \(food) on the pan!")
        print("I have \(stove) stove. Let's put the pan on the stove!")
        print("Wait a few minutes...")
        print("\(food) is ready!")
        return true
    }

    // Умение нанимать талантливых официантов:
    func hireWaiter() -> TalentedWaiter {
        let talentedWaiter = TalentedWaiter()

        // Повар учит официанта готовить самому. Он передает ему инструкцию в виде замыкания, в котором прописывает свой собственный метод cookFood:
        talentedWaiter.doEverything = { order in
            self.cookFood(order)
        }
        return talentedWaiter
    }

}

В результате после появления такого шеф-повара, найме на работу официанта, официант готовит блюдо самостоятельно, как только примет заказ:


let lazyChief = LazyChief()
let talentedWaiter = lazyChief.hireWaiter()
talentedWaiter.takeOrder("Meat")
/*
 What would you like?
 Yes, of course we have Meat!
 I have 1 pan
 Let's put Meat on the pan!
 I have 1 stove. Let's put the pan on the stove!
 Wait a few minutes...
 Meat is ready!
 */

Таким образом, как и в случае с делегатом, официант «добился» своей цели и приготовил заказ.


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

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


Поэтому последний штрих будет добавить [weak self] перед in и аргументами в передающем замыкании. Всегда помните об этом, пожалуйста!


talentedWaiter.doEverything = { [weak self] order in
            self!.cookFood(order)
        }

На этом у меня все. Надеюсь, вам было полезно. Успехов в изучении!


Скачать итоговый код можно на GitHub

Теги:delegatecallbackswiftiosделегатколбэкколбек
Хабы: Программирование Разработка под iOS Разработка мобильных приложений Swift Изучение языков
+3
4,1k 31
Комментарии 9
Лучшие публикации за сутки