Замыкания вместо делегирования в iOS 8 Swift

Screen Shot 2015-05-26 at 10.19.34 AM

При проектировании iOS приложений со многими MVC приходится решать вопросы передачи информации от одного MVC к другому MVC как в прямом, так и в обратном направлении. Передача информации в прямом направлении при переходе от одного MVC к последующему , осуществляется обычно установкой Mодели того MVC, куда мы переходим, а вот передача информации «назад» из текущего MVC в предшествующий MVC, осуществляется с помощью делегирования как в Objective-C, так и в Swift.
Кроме того, делегирование используется внутри одного MVC между View и Controller для их “слепого взаимодействия” .
Screen Shot 2015-05-25 at 9.44.52 PM
Views — слишком обощенные (generic) стандартизованные строительные блоки, они не могут что-то знать ни о классе, ни о Controller, который их использует. Views не могут владеть своими собственными данными, данные принадлежат Controller. В действительности, данные, возможно, находятся в Model, но Controller является ответственным за предоставление данных.
Тогда как же  View может общаться с Controller? С помощью делегирования.
Нужно выполнить 6 шагов, чтобы внедрить делегирование во взаимодействие View и Controller:

  1. Создаем протокол делегирования (определяем то, о чем View хочет, чтобы Controller позаботился)
  2. Создаем в View weak свойство delegate, тип которого будет протокол делегирования
  3. Используем в View свойство delegate, чтобы получать / делать вещи, которыми View  не может владеть или управлять
  4. Controller объявляет, что он реализует протокол
  5. Controller устанавливает self (самого себя) как делегата View путем установки свойства в пункте #2, приведенном выше
  6. Реализуем протокол в Controller

Мы видим, что делегирование — не простой процесс. Как в Swift, так и в Objective-C, процесс делегирования можно заменить использованием замыканий (блоков), принимая во внимание их способность захватывать любые переменные из окружающего контекста для внутреннего использования. Однако в Swift реализация этой идеи существенно упрощается и выглядит более лаконичной, так как  функции (замыкания) в Swift являются «гражданами первого сорта», то есть могут объявляться переменными и передаваться как параметры функций. Простота и абсолютная ясность кода в Swift позволят более широко использовать замыкания (closures), захватывающие контекст, для взаимодействия двух MVC или взаимодействия Controller и View без применения делегирования.

Я хочу показать использование захвата контекста замыканиями на двух примерах. Один пример будет касаться взаимодействия View  и Controller в пределах одного MVC, а другой — двух MVC. В обоих случаях  захват контекста замыканиями позволит нам заменить делегирование более простым и элегантным кодом, не требующим вспомогательных протоколов и делегатов.

В одном из Заданий раннего стэнфордского курса предлагалось разработать Графический калькулятор,
Screen Shot 2015-05-26 at 9.36.43 AM
который на iPad выглядит состоящим из двух частей: в левой части находится RPN (обратная польская запись) калькулятор, позволяющий не только проводить вычисления, но и, используя переменную M, задавать выражение для функции, которая при нажатии кнопки «График» графически воспроизводится в правой части экрана. Эти выражения можно запоминать в списке функций нажатием кнопки «Add to Favorites» и воспроизводить весь список запомненных функций с помощью кнопки «Show Favorites«. В списке вы можете выбрать любую функцию, и она будет построена в графической части. Имея набор некоторых функций, вы можете производить их графическое построение, не прибегая к RPN калькулятору.
Screen Shot 2015-05-26 at 10.19.34 AM
Кроме того, вы можете удалить ненужную функцию из списка, используя жест Swipe ( смахивания) справа налево.
Screen Shot 2015-05-26 at 10.37.15 AM

Я не буду останавливаться на реализации RPN калькулятора, процесс построения его изложен в посте. Нас будет интересовать графическая часть, и в частности, как пользовательский UIView получает информацию о координате y= f(x) от своего Controller, и как стандартный Table View, появляющийся в окошке Popover, заставляет Controller другого MVC рисовать нужный график и поддерживать синхронный список функций.
Все MVC, участвующие в приложении «Графический калькулятор» представлены ниже

Screen Shot 2015-05-26 at 10.54.04 AM

Мы видим, что используется Split View Controller, в котором роль Master стороны играет калькулятор, способный формировать функциональные зависимости типа y= f(x) , а роль Detail играет График, представляющий зависимость y= f(x).  Нас будет интересовать Detail сторона Split View Controller,  а именно MVC «График» — Favorites Graph View Controller, на котором мы отработаем взаимодействие View и Controller в пределах одного MVC, и MVC «Список функций» — Favorites Table View Controller, на котором мы отработаем его взаимодействие с MVC «График».

Захват контекста замыканием при взаимодействии View и Controller в одном MVC.

Посмотрим на  MVC «График», которое  управляется классом FavoritesGraphViewController. При внимательном рассмотрении мы обнаружим, что класс наследует от базового класса GraphViewController и содержит только то, что связано со списком функций, представленных переменной favoritePrograms, которая является массивом программ для RPN калькулятора. Вся графическая часть скрыта в базовом классе GraphViewController.
Screen Shot 2015-05-26 at 1.13.14 PM
С точки зрения поставленной в статье задачи, нам интересен именно базовый класс GraphViewController, а к классу FavoritesGraphViewController мы вернемся в следующем разделе. Это общий прием в iOS программировании, когда более обобщенный класс остается нетронутым, а все «частности» вносятся в его subclass. В данном разделе мы можем считать, что схема нашего пользовательского интерфейса имеет вид

Screen Shot 2015-05-27 at 8.34.20 AM

То есть MVC «График» управляется базовым классом GraphViewController, в который передается программа program калькулятора для построения графика ( это Mодель MVC «График»).
Screen Shot 2015-05-26 at 11.18.46 AM
View этого MVC представляет собой обычный UIView, управляемый классом GraphView.
Screen Shot 2015-05-25 at 2.10.19 PM
Перед нами поставлена задача создать абсолютно обобщенный класс GraphView, способный строить зависимости y = f(x). Этот класс ничего не должен знать о калькуляторе, он должен получать информацию о графике в виде общей зависимости y = f(x) и не хранить никаких данных. С другой стороны, в нашем Controller, представленным классом GraphViewController, как раз и содержится информация о графике y = f(x), но не в явном виде, а в виде программы program , которая может интерпретироваться экземпляром brain калькулятора 
Screen Shot 2015-05-25 at 2.37.54 PM
Имея произвольное значение x можно вычислить y c помощью калькулятора brain для установленной программы program
Screen Shot 2015-05-25 at 2.41.54 PM
Как связать эти два класса — GraphView и GraphViewController, когда у одно из них есть информация, в которой нуждается другой? Традиционный и универсальный способ выполнения этого как в Objective-C, так и в Swift — это делегирование. Об этом способе  для данного конкретного примера на Swift рассказано в  посте «Задание 3. Решение -Обязательные задания»
Мы избрали другой путь — использование замыкания (closures), захватывающего переменные из внешнего контекста, для взаимодействия двух классов, в нашем случае  GraphView и GraphViewController.
Добавляем в класс GrapherView переменную-замыкание yRorX как public, чтобы ее можно было устанавливать в GrapherViewController
Screen Shot 2015-05-25 at 4.28.12 PM
Используя Optional переменную yForX, нарисуем график в классе GrapView
Screen Shot 2015-05-25 at 4.38.20 PM
Заметьте, что для задания цепочки Optionals в случае, когда сама функция является Optional, функцию нужно взять в круглые скобки, поставить знак ? вопроса, а затем написать ее аргументы.

В GraphViewContrller в Наблюдателе didSet { } Свойства GraphView! мы установим само замыкание yForX, которое будет захватывать мой калькулятор self.brain,  и  установленную в нем  нужная программа program для построения графика.
Screen Shot 2015-05-25 at 4.43.01 PM
Все. Никаких делегатов, никаких протоколов, никаких подтверждений протоколов. Единственное — добавляем в так называемый список «захвата» [unowned self ] для исключения циклических ссылок в памяти (Лекции 9).
Код на Github.

Захват контекста замыканием при взаимодействии двух MVC.

Вернемся к варианту Графического калькулятора, способного сохранять функции графиков в специальном списке и предлагать пользователю выбирать функции из списка для графического представления

Screen Shot 2015-05-26 at 10.54.04 AM

Как было указано выше, для этого нам пришлось создать subclass класса GraphViewController, который мы назвали FavoritesGraphViewController. И теперь MVC «График»управляется классом  FavoritesGraphViewController.
В этом новом классе  FavoritesGraphViewController для списка программ мы разместим вычисляемую переменную favoritePrograms, которая является массивом программ для RPN калькулятора и связана с постоянным хранилищем NSUserDefaults. Пополнение списка программ осуществляется с помощью кнопки «Add to Favorites« — показан  @IBAction func addFavorite (). К массиву favoritePrograms добавляется текущая программа program
Screen Shot 2015-05-26 at 6.20.15 PM
Для отображения списка программ используется другой MVCMVC «Список функций». Это обычный Table View Controller, которым управляет класс FavoriteTableViewController. «Переезд» на MVC «Список функций» осуществляется при нажатии кнопки «Show Favorites«, которая находится на MVC «График», с помощью segue типа «Present as Popover»
Screen Shot 2015-05-26 at 7.27.18 PM
Моделью для класса FavoriteTableViewController является массив программ для RPN калькулятора, который нужно отобразить в таблице
Screen Shot 2015-05-26 at 8.26.49 PM
Выполняем методы Table View DataSource
Screen Shot 2015-05-26 at 8.49.00 PM
И сразу же сталкиваемся с тем, что нам нужно отображать в строке таблицы не программу для RPN калькулятора, а ее описание в «цивилизованном инфиксном» виде, ведь наш MVC называется MVC «Список функций». Для этого надо запрашивать калькулятор, который находится в MVC «График».
Добавляем в класс FavoriteTableViewController переменную-замыкание descriptionProgram, тип которой — функция, имеющая на входе два параметра:

  • FavoriteTableViewController — класс, который запрашивает этот метод
  • index — индекс программы в списке программ favoritePrograms, которой нужно инфиксное описание

На выходе получается Optional  строка c описанием
Screen Shot 2015-05-26 at 8.57.22 PM
Это замыкание мы будем устанавливать в MVC «График» в процессе подготовки к «переезду» на MVC «Список функций» в методе prepareForSegue
Screen Shot 2015-05-27 at 4.07.40 AM
Замыкание descriptionProgram захватит в MVC «График» программу калькулятора и массив программ и будет их использовать при каждом вызове.

Вернемся к нашей таблице и классу FavoriteTableViewController. Нам нужно обеспечить рисование соответствующего графика при выборе определенной функции в таблице. Будем использовать метод делегата didSelectRowAtIndexPath.

Screen Shot 2015-05-27 at 5.14.21 AM

В этом методе нам нужно заставить другой MVC — MVC «График» нарисовать нужный график. И опять, добавляем в класс FavoriteTableViewController переменную-замыкание didSelect, тип которой — функция, имеющая на входе два параметра:

  • FavoriteTableViewController — класс, который запрашивает этот метод
  • index — индекс программы в списке программ favoritePrograms, которой нужно инфиксное описание

Screen Shot 2015-05-27 at 4.26.27 AM
Это замыкание didSelect мы будем устанавливать в MVC «График» в процессе подготовки к переезду на MVC «Список функций» в методе prepareForSegue

Screen Shot 2015-05-27 at 4.37.53 AM
Замыкание didSelect захватит в MVC «График» программу program, которая устанавливается для калькулятора извне, и переустановить ее, что заставит MVC «График» перерисовать нужный нам график. В этом же замыкании вы можете убрать Popover окно со списком функций с экрана (достаточно убрать комментарий со строки controller.dismissControlerAnimated) или оставить его для последующего выбора пользователем.
И опять возвращаемся к нашей таблице и классу FavoriteTableViewController. Нам нужно обеспечить удаление строки в списке функций. Будем использовать метод делегата commitEditingStyle
Screen Shot 2015-05-27 at 4.57.21 AM
Конечно, мы предпринимаем все необходимые меры для удаления строки из таблицы : удаляем из Модели programs, визуально удаляем столбец из таблицы, но этого недостаточно. Нам нужно синхронизовать удаление с массивом программ, находящемся в постоянном хранилище NSUserDefaults, а это опять находится в другом MVCMVC «График».
И опять, добавляем в класс FavoriteTableViewController переменную-замыкание didDelete, тип которой — функция, имеющая на входе два параметра:

  • FavoriteTableViewController — класс, который запрашивает этот метод
  • index — индекс программы в списке программ favoritePrograms, которой нужно инфиксное описание

Screen Shot 2015-05-27 at 5.10.06 AM

Это замыкание didDelete мы будем устанавливать в MVC «График» в процессе подготовки к переезду на MVC «Список функций» в методе prepareForSegue

Screen Shot 2015-05-27 at 5.17.07 AM
Замыкание didDelete захватывает массив программ favoritePrograms, связанный с постоянным хранилищем NSUserDefaults,  и удаляет соответствующую программу.

Итак, мы рассмотрели как MVC «Список функций» взаимодействует с вызвавшим его MVC «График» в обратном направлении с помощью замыканий. Теперь рассмотрим прямое взаимодействие. Где же устанавливается Модель programs для MVC «Список функций»? Мы будем устанавливать ее в MVC «График» в процессе подготовки к переезду на MVC «Список функций» в том же методе prepareForSegue
Screen Shot 2015-05-27 at 5.29.25 AM

Итак, схема использования замыканий для обмена информацией между различными MVC очень простая.
Она состоит из 3-х шагов:

  • В MVC, требующим взаимодействия, создаете public переменную — замыкание
  • Используете ее в том же MVC
  • В другом MVC устанавливаете это замыкание либо в Наблюдателе Свойств  didSet {}, либо в методе  prepareForSegue, либо еще где-то так, чтобы замыкание «захватило» нужные переменные и константы

Все.
Никаких вспомогательных элементов — протоколов и делегатов. Код для Swift 1.2 находится на Github. Код для Swift 2.0 находится на Github.

Заключение

Мы рассмотрели передачи информации от одного MVC к другому MVC как в прямом, так и в обратном направлении. Передача информации в прямом направлении при переходе от одного MVC к последующему, осуществляется установкой Mодели того MVC, куда мы переходим. Передачу информации «назад» из текущего MVC в предшествующий MVC очень удобно и легко осуществлять в Swift с помощью замыканий.

Этот прием можно используется также и внутри одного MVC для “слепого взаимодействия” между View и Controller. Представлен демонстрационный пример Графический Калькулятор, который показывает все эти возможности.

Обращаю ваше внимание, что условием разработки Графического калькулятора было создание классов, поддерживающих построения графика и вывод списка функций в табличном виде, как можно более обобщенными (generic), не знающими ничего о существовании RPN калькулятора. Поэтому все переменные — замыкания во всех представленных примерах имеют очень обобщенный (generic) вид, связанный исключительно с семантикой соответствующих классов GraphView и FavoriteTableViewController.

Замыкания вместо делегирования в iOS 8 Swift: 4 комментария

  1. Благодарю за интересную статью! Приём с использованием замыканий для передачи данных выглядит действительно элегантно! 🙂

    • Еще это можно увидеть в Задании 6 за 2016 в игре Breakout, когда мячик ударяет кирпич (это коллайдер), а ты должен показать в главном ViewController счет, показать анимацию «кирпича», который должен анимировать :

  2. Такие статьи тоже бы хотелось в pdf!
    Но в любом случае большое спасибо за ваш труд!

    • На самом деле здесь немного сложновато. Прекрасный пример использования замыкания для выполнения кода в другом классе профессор представил в самом начале Лекции 13 курса iOS 11 и Swift 4, когда рассказывает о выполнении кода в текстовом поле по корректировке коллекции Collection View. Посмотрите Лекцию 13, если нужен русский язык, то переведу через 3-4 дня и выложу.

Обсуждение закрыто.