Около двух лет назад кто-то задал мне хороший вопрос: «Почему нам нужны делегаты для UIViewControllers?» Он думал, что Swift многое облегчил, но вся эта штука с делегатами кажется очень сложной. Почему просто нельзя посылать сообщения или инициализации между классами?
Когда я впервые изучал iOS, я признал, что у меня ушли месяцы, чтобы понять, что произошло с делегацией. Я нашел много непонятного кода и немного объяснений. Когда я работал над этим, результата было мало. В большинстве случаев туториалы ссылались на информацию о том, как использовать стандартный делегат Apple, но не показывали, как создавать свой отклик. Эти отклики необходимы для полного понимания делегатов.
Я решил, что время обновить статью и включить два примера с которыми разработчики могут столкнуться: iOS и watchOS версии. Вместе с взрослением watchOS в watchOS 3, я думаю, многие разработчики начнут смотреть в сторону разработки приложений для часов и там могут столкнуться с непонятными вещами.
Давайте начнем с начала, что бы все понимали проблему. До тех пор, пока мы используем классы в объектно-ориентированном программировании, стоит хорошо понимать, что они из себя представляют. Class — это коллекция данных, которые мы называем properties (свойства) и действий methods (методы) к properties.
Properties и methods могут быть public (публичными) или private (приватными). Public method видят и используют классы, кроме определяющего. Private значит, что properties или methods видны и используются внутри определяющего класса, а другие классы не в состоянии увидеть или воспользоваться ими. В Swift private делает properties и methods приватными. Вычисление properties это другой способ сделать properties приватными. Кроме того, в Swift существует стандартное состояние, которое делает method или class публичным только для текущей цели.
Многим разработчикам не нравится, когда играются с их кодом, а это тоже хороший опыт программирования. В целом, стоит стремиться к тому, чтобы оставлять публичным необходимый минимум для других классов. Сохранение properties и methods приватными и сокрытие классов называется encapsulation (инкапсуляция).
Инкапсуляция позволяет строить код, как если бы мы строили дом из блоков. Как и у обычного кирпича есть несколько применений, так и у класса есть несколько используемых methods. После этого они могут присоединять множество других кирпичей.
Часто встречаемый термин MVC среди разработчиков, это не эксклюзивный термин для Xcode проектов. Это последовательность программирования, хорошая организация любой программы или приложения в графическом окружении и в целом окружении, которое взаимодействует с пользователем. MVC разделяет главные части приложения. Во первых разделяет данные и взаимодействие с пользователем и добавляет посредника между ними. Почему это так важно? Вы можете написать и опубликовать приложение для iPhone, затем решить, что версия для iPad это неплохая идея, а затем решить добавить версию для часов. С MVC вы поменяете только одну часть полностью — View и, возможно, немного Controller. Код, содержащий данные никогда не меняется между версиями. Это сохранит много времени и сил.
Существуют части программы работающие с данными, которые мы хотим обработать. Система заказа пиццы имеет данные о любом заказе, где могут содержаться ссылки на более подробные данные о каждом клиенте и пицце. Это наша Model в системе заказа пиццы: коллекция всех данных, используемых в системе заказов. Она никак не должна контактировать с пользователем, ничего не просит указать и не отображает. Это просто данные. Вот пример простой модели:
Эта модель состояния переключателя. Это данные. Модель имеет два methods: textState() — описывает состояние переключателя, как String, overLoadSwitch() выключает переключатель, если количество пользователей * загрузку больше 100. Есть еще много методов, которые я должен добавить для описания переключателя, но любые методы изменяют или описывают только данные. Тут нет никакого ввода данных от пользователя. Model может выполнять расчеты, но опять же, тут нет никакого взаимодействия с пользователем.
В View происходят все взаимодействия с пользователем. Большинство людей в Xcode используют Interface Builder, как холст, чтобы использовать storyboard или .xib для построения View. Разработчик может программно создавать View класс, который содержит различные элементы управления. Как Model никогда не взаимодействует с пользователем, View никогда не взаимодействует напрямую с данными. View ничего не делает, он просто есть. Он может отвечать на касания пользователя, например, передавая сигнал какому-либо методу, меняя цвет, если кнопка нажимается или перелистывает список, но это все, что он делает. View содержит множество свойств и методов и говорит нам о состоянии View. Мы можем изменять поведение интерфейса или его вид через методы или свойства. View может передать Controller, что были изменения в View, например, нажата кнопка или введен символ. Но он ничего не может сделать с этим, кроме как передать информацию.
Сердце MVC объединяет View и Model. Называемый Controller или ViewController, координирует то, что происходит в данных или View. Если пользователь нажимает кнопку, Controller отвечает на это событие. Если этот ответ означает посыл сообщения Model, ViewController делает это. Если ответ требует вывода данных из Model, Controller делает и это тоже. В Xcode, @IBOutlet и @IBAction объединяют файлы содержащие views в Interface Builder и ViewController.
Ключ к MVC — коммуникация. Если быть точным, ее недостаток. MVC воспринимает инкапсуляцию очень серьезно. View и Model никогда не общаются напрямую между собой. Controller может отсылать сообщения View и Controller. View и Controller могут выполнять внутренние действия в ответ на сообщения, как вызов метода или могут возвращать значение Controller’у. Controller никогда не производит изменения напрямую ни в Model, ни в View.
Итак, View, Model и Controller не изменяют свойства друг друга. View и Model могут не общаться друг с другом вообще. View может говорить Controller, что есть изменение в Model. Controller может посылать сообщения в форме вызова методов к View, Model и принимать ответы через этот метод.
Все, что мы сейчас обсудили, касается только одной scene в довольно большом приложении. Предположим, у меня есть подобный watchOS storyboard:
Имеется кнопка Switch, которая загружает другой View, имеющий переключатель. Когда я решу, как должен располагаться переключатель, я нажму Done.
В Xcode у нас есть segues (сегвей). Сегвеи удобны для указания от одного ViewController к другому. Когда сегвей делает сложные для нас вещи проще и мы передвигаемся от одного Controller к другому, сегвей говорит системе открыть определенный ViewController, а затем открывает View и Model.
Model и View в новой MVC настройке отличаются от вызвавшего. Apple включила метод prepare(for segue:) для iOS, который дает нам шанс установить значения в новом ViewController и соответственно в новых ViewController’s View и Model.
С появлением WatchOS появился слегка другой подход. Вместо доступа к полной Model, watchOS отправляет новым контроллерам единственное значение, называемое context. Так как оно типа Any?, вы можете передавать ему любые значения. Чаще всего разработчики передают dictionaries значений другим контроллерам через contextForSegue метод.
Для идентичных приложений для iPhone и часов я могу нажать кнопку Switch. Она запускает интерфейс переключателя и передает значение переключателю false.
Мы можем включать и выключать переключатель довольно легко, но когда мы нажимаем Done, чтобы отправить его обратно оригинальному контроллеру вот тогда и появляется проблема. По правилам MVC нам нужен метод, возвращающий значение. Где в вызываемом instance мы можем вернуться к его вызвавшему его классу?
С инкапсулированным классом мы не можем. В данной ситуации нет возможности отправить пересмотренную Model обратно оригинальному контроллеру без поломки инкапсуляции или MVC. Новый ViewController ничего не знает о классе который его вызвал. Похоже мы застряли. Если мы попытаемся создать reference напрямую к вызвавшему контроллеру, мы вызовем reference loop, который убьет память. Проще говоря, мы не можем отправлять данные обратно.
Эту проблему делегаты и протоколы решают будучи немного проворными. Представьте себе другой класс, который на самом деле является скелетом класса. Этот класс содержит только методы. Он декларирует определенные методы в этом классе, но никогда не использует их. В Swift они называются протоколы. Мы создаем протокол, содержащий один метод. Этот метод это то, что вы делаете когда заканчиваете с переключателем и хотите вернуться к вызвавшему контроллеру. Он имеет несколько параметров, данных для передачи обратно вызвавшему контроллеру. Он может выглядеть так:
Я передал обратно значение переключателя в данном случае.
В контроллере с переключателем мы создаем instance этого протокола и называем его делегат.
Так как мы имеем свойство типа SwitchDelegate, мы можем использовать методы типа SwitchDelegate, например метод didFinishSwitch. Мы можем привязать выполнение данного метода к кнопке Done:
или для WatchOS:
Как только вы сделаете это, вы получите ошибку компиляции, так как метод протокола не существует в классе. В коде, для адаптации класса на примере OrderPizzaViewController, мы используем метод:
Мы возвращаем данные обратно и то, что нам нужно с ними. В этом случае, будет присвоен текст, отображаемый в Label.
Еще один шаг. Во время возврата в назначенный контроллер, я сказал, что делегат является instanc’ом протокола, но я не сказал, где делегат находился. В prepare(for Segue:) я добавил дополнительную строчку vc.delegate = self, говорящий протоколу о вашем контроллере:
В WatchOS все становится немного по другому. У меня есть только один передаваемый context, switchState и делегату. Для нескольких значений разработчики обычно используют dictionary вроде этого:
awake метод имеет код для извлечения dictionary и присвоения значений. Когда мы нажимаем Done на часах или в приложении, запускается метод, он знает, что находится в вызывающем контроллере и будет вызван там, где мы добавили его к оригинальному классу. data это параметр, что бы программа могла с легкостью переносить в контроллер и в Model. Делегаты и протоколы довольно хитрые, но это работает и является одной из самых важных техник при работе с ViewController’ами.
Основной вопрос заключался в том, почему Swift не сделал все это проще. Так как я показал тут все в контексте Swift, не имеет значения какой объектно — ориентированный язык вы используете. Делегация это часть последовательности MVC, которая является важным навыком программирования.
Перевод вот этой статьи
Когда я впервые изучал iOS, я признал, что у меня ушли месяцы, чтобы понять, что произошло с делегацией. Я нашел много непонятного кода и немного объяснений. Когда я работал над этим, результата было мало. В большинстве случаев туториалы ссылались на информацию о том, как использовать стандартный делегат Apple, но не показывали, как создавать свой отклик. Эти отклики необходимы для полного понимания делегатов.
Я решил, что время обновить статью и включить два примера с которыми разработчики могут столкнуться: iOS и watchOS версии. Вместе с взрослением watchOS в watchOS 3, я думаю, многие разработчики начнут смотреть в сторону разработки приложений для часов и там могут столкнуться с непонятными вещами.
Что такое Class?
Давайте начнем с начала, что бы все понимали проблему. До тех пор, пока мы используем классы в объектно-ориентированном программировании, стоит хорошо понимать, что они из себя представляют. Class — это коллекция данных, которые мы называем properties (свойства) и действий methods (методы) к properties.
Properties и methods могут быть public (публичными) или private (приватными). Public method видят и используют классы, кроме определяющего. Private значит, что properties или methods видны и используются внутри определяющего класса, а другие классы не в состоянии увидеть или воспользоваться ими. В Swift private делает properties и methods приватными. Вычисление properties это другой способ сделать properties приватными. Кроме того, в Swift существует стандартное состояние, которое делает method или class публичным только для текущей цели.
Многим разработчикам не нравится, когда играются с их кодом, а это тоже хороший опыт программирования. В целом, стоит стремиться к тому, чтобы оставлять публичным необходимый минимум для других классов. Сохранение properties и methods приватными и сокрытие классов называется encapsulation (инкапсуляция).
Инкапсуляция позволяет строить код, как если бы мы строили дом из блоков. Как и у обычного кирпича есть несколько применений, так и у класса есть несколько используемых methods. После этого они могут присоединять множество других кирпичей.
Что такое Model — View — Controller или MVC?
Часто встречаемый термин MVC среди разработчиков, это не эксклюзивный термин для Xcode проектов. Это последовательность программирования, хорошая организация любой программы или приложения в графическом окружении и в целом окружении, которое взаимодействует с пользователем. MVC разделяет главные части приложения. Во первых разделяет данные и взаимодействие с пользователем и добавляет посредника между ними. Почему это так важно? Вы можете написать и опубликовать приложение для iPhone, затем решить, что версия для iPad это неплохая идея, а затем решить добавить версию для часов. С MVC вы поменяете только одну часть полностью — View и, возможно, немного Controller. Код, содержащий данные никогда не меняется между версиями. Это сохранит много времени и сил.
Что такое Model?
Существуют части программы работающие с данными, которые мы хотим обработать. Система заказа пиццы имеет данные о любом заказе, где могут содержаться ссылки на более подробные данные о каждом клиенте и пицце. Это наша Model в системе заказа пиццы: коллекция всех данных, используемых в системе заказов. Она никак не должна контактировать с пользователем, ничего не просит указать и не отображает. Это просто данные. Вот пример простой модели:
class switchState{
var state:Bool
func textState()->String{
if state {
return "On"
} else {
return "Off"
}
func overLoadSwitch(users:Int,load:Int){
let totalLoad = users * load
if totalLoad > 100 {
switch = false
}
}
Эта модель состояния переключателя. Это данные. Модель имеет два methods: textState() — описывает состояние переключателя, как String, overLoadSwitch() выключает переключатель, если количество пользователей * загрузку больше 100. Есть еще много методов, которые я должен добавить для описания переключателя, но любые методы изменяют или описывают только данные. Тут нет никакого ввода данных от пользователя. Model может выполнять расчеты, но опять же, тут нет никакого взаимодействия с пользователем.
Что такое View?
В View происходят все взаимодействия с пользователем. Большинство людей в Xcode используют Interface Builder, как холст, чтобы использовать storyboard или .xib для построения View. Разработчик может программно создавать View класс, который содержит различные элементы управления. Как Model никогда не взаимодействует с пользователем, View никогда не взаимодействует напрямую с данными. View ничего не делает, он просто есть. Он может отвечать на касания пользователя, например, передавая сигнал какому-либо методу, меняя цвет, если кнопка нажимается или перелистывает список, но это все, что он делает. View содержит множество свойств и методов и говорит нам о состоянии View. Мы можем изменять поведение интерфейса или его вид через методы или свойства. View может передать Controller, что были изменения в View, например, нажата кнопка или введен символ. Но он ничего не может сделать с этим, кроме как передать информацию.
Что такое Controller?
Сердце MVC объединяет View и Model. Называемый Controller или ViewController, координирует то, что происходит в данных или View. Если пользователь нажимает кнопку, Controller отвечает на это событие. Если этот ответ означает посыл сообщения Model, ViewController делает это. Если ответ требует вывода данных из Model, Controller делает и это тоже. В Xcode, @IBOutlet и @IBAction объединяют файлы содержащие views в Interface Builder и ViewController.
Ключ к MVC — коммуникация. Если быть точным, ее недостаток. MVC воспринимает инкапсуляцию очень серьезно. View и Model никогда не общаются напрямую между собой. Controller может отсылать сообщения View и Controller. View и Controller могут выполнять внутренние действия в ответ на сообщения, как вызов метода или могут возвращать значение Controller’у. Controller никогда не производит изменения напрямую ни в Model, ни в View.
Итак, View, Model и Controller не изменяют свойства друг друга. View и Model могут не общаться друг с другом вообще. View может говорить Controller, что есть изменение в Model. Controller может посылать сообщения в форме вызова методов к View, Model и принимать ответы через этот метод.
Как MVC связывается с другими MVC
Все, что мы сейчас обсудили, касается только одной scene в довольно большом приложении. Предположим, у меня есть подобный watchOS storyboard:
Имеется кнопка Switch, которая загружает другой View, имеющий переключатель. Когда я решу, как должен располагаться переключатель, я нажму Done.
Легкий путь
В Xcode у нас есть segues (сегвей). Сегвеи удобны для указания от одного ViewController к другому. Когда сегвей делает сложные для нас вещи проще и мы передвигаемся от одного Controller к другому, сегвей говорит системе открыть определенный ViewController, а затем открывает View и Model.
Model и View в новой MVC настройке отличаются от вызвавшего. Apple включила метод prepare(for segue:) для iOS, который дает нам шанс установить значения в новом ViewController и соответственно в новых ViewController’s View и Model.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "switch"{
let vc = segue.destination as! SwitchViewController
vc.switchState = false
}
С появлением WatchOS появился слегка другой подход. Вместо доступа к полной Model, watchOS отправляет новым контроллерам единственное значение, называемое context. Так как оно типа Any?, вы можете передавать ему любые значения. Чаще всего разработчики передают dictionaries значений другим контроллерам через contextForSegue метод.
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
return self
}
Для идентичных приложений для iPhone и часов я могу нажать кнопку Switch. Она запускает интерфейс переключателя и передает значение переключателю false.
Проблемный путь
Мы можем включать и выключать переключатель довольно легко, но когда мы нажимаем Done, чтобы отправить его обратно оригинальному контроллеру вот тогда и появляется проблема. По правилам MVC нам нужен метод, возвращающий значение. Где в вызываемом instance мы можем вернуться к его вызвавшему его классу?
С инкапсулированным классом мы не можем. В данной ситуации нет возможности отправить пересмотренную Model обратно оригинальному контроллеру без поломки инкапсуляции или MVC. Новый ViewController ничего не знает о классе который его вызвал. Похоже мы застряли. Если мы попытаемся создать reference напрямую к вызвавшему контроллеру, мы вызовем reference loop, который убьет память. Проще говоря, мы не можем отправлять данные обратно.
Эту проблему делегаты и протоколы решают будучи немного проворными. Представьте себе другой класс, который на самом деле является скелетом класса. Этот класс содержит только методы. Он декларирует определенные методы в этом классе, но никогда не использует их. В Swift они называются протоколы. Мы создаем протокол, содержащий один метод. Этот метод это то, что вы делаете когда заканчиваете с переключателем и хотите вернуться к вызвавшему контроллеру. Он имеет несколько параметров, данных для передачи обратно вызвавшему контроллеру. Он может выглядеть так:
protocol SwitchDelegate {
func didFinishSwitch(switchState:Bool)
}
Я передал обратно значение переключателя в данном случае.
В контроллере с переключателем мы создаем instance этого протокола и называем его делегат.
delegate : SwitchDelegate! = nil
Так как мы имеем свойство типа SwitchDelegate, мы можем использовать методы типа SwitchDelegate, например метод didFinishSwitch. Мы можем привязать выполнение данного метода к кнопке Done:
@IBAction func doneButtonPressed(sender:UIButton!){
delegate.didFinishSwitch(switchState: switchState)
dismiss(animated: true, completion: nil)
}
или для WatchOS:
@IBAction func submitSwitchStatus() {
delegate.didFinishSwitch(switchState: switchState)
pop()
}
Как только вы сделаете это, вы получите ошибку компиляции, так как метод протокола не существует в классе. В коде, для адаптации класса на примере OrderPizzaViewController, мы используем метод:
func didFinishSwitch(switchState: Bool) {
if switchState {
textState = "Switch is On"
} else {
textState = "Switch is Off"
}
}
Мы возвращаем данные обратно и то, что нам нужно с ними. В этом случае, будет присвоен текст, отображаемый в Label.
Еще один шаг. Во время возврата в назначенный контроллер, я сказал, что делегат является instanc’ом протокола, но я не сказал, где делегат находился. В prepare(for Segue:) я добавил дополнительную строчку vc.delegate = self, говорящий протоколу о вашем контроллере:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "switch"{
let vc = segue.destination as! SwitchViewController
vc.switchState = false
vc.delegate = self
}
В WatchOS все становится немного по другому. У меня есть только один передаваемый context, switchState и делегату. Для нескольких значений разработчики обычно используют dictionary вроде этого:
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
let context:[String:Any] = ["switchState":false,"delegate":self]
return context
}
awake метод имеет код для извлечения dictionary и присвоения значений. Когда мы нажимаем Done на часах или в приложении, запускается метод, он знает, что находится в вызывающем контроллере и будет вызван там, где мы добавили его к оригинальному классу. data это параметр, что бы программа могла с легкостью переносить в контроллер и в Model. Делегаты и протоколы довольно хитрые, но это работает и является одной из самых важных техник при работе с ViewController’ами.
Основной вопрос заключался в том, почему Swift не сделал все это проще. Так как я показал тут все в контексте Swift, не имеет значения какой объектно — ориентированный язык вы используете. Делегация это часть последовательности MVC, которая является важным навыком программирования.
Перевод вот этой статьи