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

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

Кроме того, делегирование используется внутри одного MVC между View и Controller для их «слепого взаимодействия».

Дело в том, что Views — слишком обощенные (generic) стандартизованные строительные блоки, они не могут что-то знать ни о классе, ни о Controller, который их использует. Views не могут владеть своими собственными данными, данные принадлежат Controller. В действительности, данные могут находиться в Mодели, но Controller является ответственным за их предоставление. Тогда как же  View может общаться с Controller? С помощью делегирования.

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

Я хочу показать использование захвата контекста замыканиями на двух примерах, взятых из стэнфордского курса 2015 «Developing iOS 8 Apps with Swift» (русский эквивалент находится на сайте «Разработка iOS+Swift+Objective-C приложений»).

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

09bb551bd5b74eefbafa9acc93ec39b9.png

который на iPad выглядит состоящим из двух частей: в левой части находится RPN (обратная польская запись) калькулятор, позволяющий не только проводить вычисления, но и, используя переменную M, задавать выражение для функции, которая при нажатии кнопки «График» графически воспроизводится в правой части экрана. Эти выражения можно запоминать в списке функций нажатием кнопки «Add to Favorite» и воспроизводить весь список запомненных функций с помощью кнопки «Favorites». В списке вы можете выбрать любую функцию (рисунок в заголовке), и она будет построена в графической части. Имея набор некоторых функций, вы можете производить их графическое построение, не прибегая к RPN калькулятору.Кроме того, вы можете удалить ненужную функцию из списка, используя жест Swipe (смахивания) справа налево.

90e89e9818b24945bb9996f6b353f1df.png

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

87f2c9b5db6844b1a5d63fa8334385cc.png

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

Захват контекста замыканием при взаимодействии View и Controller в одном MVC.Посмотрим на MVC «График», которое управляется классом FavoritesGraphViewController.c29ffe66c74c47d2896e42aed0125b76.png

При внимательном рассмотрении мы обнаружим, что класс FavoritesGraphViewController наследует от базового класса GraphViewController и содержит только то, что связано со списком функций, представленном переменной favoritePrograms, которая является массивом программ для RPN калькулятора. Вся графическая часть скрыта в базовом классе GraphViewController. С точки зрения поставленной в статье задачи, нам интересен именно базовый класс GraphViewController, а к классу FavoritesGraphViewController мы вернемся в следующем разделе. Это общий прием в iOS программировании, когда более обобщенный класс остается нетронутым, а все «частности» вносятся в его subclass. В данном разделе мы можем считать, что схема нашего пользовательского интерфейса имеет более упрощенный вид:

97954bdc18b44545839ad4ad80d86d04.png

То есть MVC «График» управляется классом GraphViewController, в который передается программа program RPN калькулятора для построения графика (это Mодель MVC «График»).

d6d6a83904e047b1ac565559d61817b5.png

View этого MVC представляет собой обычный UIView, управляемый классом GraphView.

3c8860882eb34a5e988a0b261c722b7e.png

Перед нами поставлена задача создать абсолютно обобщенный класс GraphView, способный строить зависимости y = f (x). Этот класс ничего не должен знать о калькуляторе, он должен получать информацию о графике в виде общей зависимости y = f (x) и не хранить никаких данных. С другой стороны, в нашем Controller, представленным классом GraphViewController, как раз и содержится информация о графике y = f (x), но не в явном виде, а в виде программы program, которая может интерпретироваться экземпляром brain RPN калькулятора.

183b5e9389924b4e92477b7cbecf57bc.png

Имея произвольное значение x можно вычислить y c помощью калькулятора brain для установленной программы program

8c03b37df98b4ae18bcdecedd007c090.png

Как связать эти два класса — GraphView и GraphViewController, когда у одно из них есть информация, в которой нуждается другой? Традиционный и универсальный способ выполнения этого как в Objective-C, так и в Swift — это делегирование. Об этом способе для данного конкретного примера на Swift рассказано в посте «Задание 3. Решение -Обязательные задания».

Мы избрали другой путь — использование замыкания (closures), захватывающего переменные из внешнего контекста, для взаимодействия двух классов, в нашем случае GraphView и GraphViewController.

Добавляем в класс GrapherView переменную-замыкание yForX как public (not private), чтобы ее можно было устанавливать в GrapherViewController

c3ea2458777e4fada06ecd259524fb1e.png

Используя Optional переменную yForX, нарисуем график в классе GrapView:

11b37038cffb4fc0a0080ba68281b0f1.png

Заметьте, что для задания цепочки Optionals в случае, когда сама функция является Optional, функцию нужно взять в круглые скобки, поставить знак? вопроса, а затем написать ее аргументы.В GraphViewController в Наблюдателе didSet { } Свойства GraphView! , которое является @IBOutlet, мы установим замыкание yForX так, чтобы оно захватило ссылку на экземпляр моего калькулятор self.brain, в котором уже установлена нужная программа program для построения графика. Каждый раз при обращении к yForX будет использоваться один и тот же «захваченный» калькулятор, а это то, что нам нужно.

d9dd860d221f44ba92e932855b9a6374.png

Все. Никаких делегатов, никаких протоколов, никаких подтверждений протоколов. Единственное — добавляем в так называемый список «захвата» [unowned self ] для исключения циклических ссылок в памяти (об этом рассказывается в Лекции 9 курса «Developing iOS 8 Apps with Swift»).

Код на Github.

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

Как было указано выше, для этого нам пришлось создать subclass класса GraphViewController, который мы назвали FavoritesGraphViewController. И теперь MVC «График», управляется классом FavoritesGraphViewController.В этом новом классе FavoritesGraphViewController для списка программ мы разместим вычисляемую переменную favoritePrograms, которая является массивом программ для RPN калькулятора и связана с постоянным хранилищем NSUserDefaults. Пополнение списка программ осуществляется с помощью кнопки «Add to Favorite». К массиву favoritePrograms добавляется текущая программа program

a71c6542834341bc87bc2c095fbae35a.png

Для отображения списка программ используется другой MVC — MVC «Список функций». Это обычный Table View Controller, которым управляет класс FavoriteTableViewController. «Переезд» на MVC «Список функций» осуществляется при нажатии кнопки «Show Favorites», которая находится на MVC «График», с помощью segue типа «Present as Popover».

Моделью для класса FavoriteTableViewController является массив программ для RPN калькулятора, который нужно отобразить в таблице.

ef6c0ce9cea249338857e8085bb46faa.png

Выполняем методы Table View DataSource

b67dd1a8be8b4164ac6fe333e07cc28d.pngИ сразу же сталкиваемся с тем, что нам нужно отображать в строке таблицы не программу для RPN калькулятора, а ее описание в «цивилизованном инфиксном» виде, ведь наш MVC называется MVC «Список функций». Для этого надо запрашивать калькулятор, который находится в MVC «График».

Добавляем в класс FavoriteTableViewController переменную-замыкание descriptionProgram, тип которой — функция, имеющая на входе два параметра:

FavoriteTableViewController — класс, который запрашивает этот метод index — индекс программы в списке программ favoritePrograms, которой нужно инфиксное описание На выходе получается Optional строка c описанием: f0460bf896ed445ca7da99c09c7b075c.png

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

31b398aaed9141a68420e08eeef97809.png

Замыкание descriptionProgram захватит в MVC «График» программу калькулятора и массив программ и будет их использовать при каждом вызове.

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

FavoriteTableViewController — класс, который запрашивает этот метод index — индекс программы в списке программ favoritePrograms, которой нужно инфиксное описание Эти функции ничего не возвращают, так как все действия производятся внутри замыканий: 48e4ba2828bf4ec39897ca41dba186ba.png

Будем использовать методы делегата didSelectRowAtIndexPath и commitEditingStyle… и только что объявленные переменные-замыкания для выполнения поставленных задач:

b42fc8fbfa2e4a6c9487e48e75a67eca.png

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

03bf371b7d3348178190ec513b3935ca.png

Замыкание didSelect захватит в MVC «График» программу program, которая устанавливается для калькулятора извне, и переустановит ее, что заставит MVC «График» перерисовать нужный нам график. В этом же замыкании вы можете убрать Popover окно со списком функций с экрана (достаточно убрать комментарий со строки controller.dismissControlerAnimated…) или оставить его для последующего выбора пользователем.

Замыкание didDelete захватит массив программ favoritePrograms, связанный с постоянным хранилищем NSUserDefaults, и удаляет соответствующую программу.Итак, мы рассмотрели как MVC «Список функций» взаимодействует с вызвавшим его MVC «График» в обратном направлении с помощью замыканий.

Теперь рассмотрим прямое взаимодействие. Где же устанавливается Модель programs для MVC «Список функций»? Мы будем устанавливать ее в MVC «График» в процессе подготовки к переезду на MVC «Список функций» в том же методе prepareForSegue

105b1c69c0d843469ce3933bd07c1f15.png

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

Код на Github.

На iPhone использование Графического калькулятора еще эффективнее, так как там работает не Split View Controller, а Navigation Controller, и вы остаетесь один на один со списком функций на экране.

f47d236aab204d669f550ab0b43ed2fb.png

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

Ссылки Стэнфордский курс 2015 «Developing iOS 8 Apps with Swift» Русский неавторизованный конспект лекций и решения Заданий находятся на сайте «Разработка iOS+Swift+Objective-C приложений«Текст Задания 3 на английском языке доступен на iTunes в пункте «Developing iOS 8 app: Programming: Project 3″.Текст Задания 3 на русском языке доступен на «Задание 3 iOS 8.pdf«Решение Задания 3 «Графический калькулятор» с нуля.Задание 3 cs193p Зима 2015 Графический Калькулятор. Решение — обязательные пунктыЗадание 3 cs193p Зима 2015 Графический Калькулятор. Решение — дополнительные пункты 1, 2 и 3Задание 3. Решение — дополнительные пункты 4, 5 и 6. Окончание.Код на Github.

© Habrahabr.ru