Замыкания вместо делегирования в 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
Для отображения списка программ используется другой MVC — MVC «Список функций». Это обычный 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, а это опять находится в другом MVC — MVC «График».
И опять, добавляем в класс 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 дня и выложу.

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