При проектировании 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:

  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 без применения делегирования.

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

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

В Заданиях стэнфордского курса предлагается разработать Графический калькулятор,



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



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



Мы видим, что используется 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.



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



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



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



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



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



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

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

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



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



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



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

Код на Github.

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


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



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



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

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



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


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

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

На выходе получается Optional строка c описанием:



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



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

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

Эти функции ничего не возвращают, так как все действия производятся внутри замыканий:



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



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



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

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

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



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

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

Все.
Никаких вспомогательных элементов — протоколов и делегатов.

Код на Github.

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



Заключение


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

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

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

Ссылки


Стэнфордский курс 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.
Примечание. Если будете экспериментировать с Графическим калькулятором, то помните, что это RPN калькулятор, поэтому сначала вводятся операнды, а потом операция. Чтобы получить функцию sin (1/M) нужно ввести на калькуляторе следующую последовательность символов
1 ? M ? sin кнопка «График» дает sin (1/M)
M cos M ? кнопка «График» дает cos(M)*M
M ? 1 ? M sin + ? кнопка «График» дает M * ( 1 +sin (M))

Комментарии (12)


  1. visput
    31.05.2015 20:53

    Вы так написали, как будто возможность использования замыканий вместо делегирования появилась только в Swift.
    В Objective-C замыкания (блоки) появились 5 лет назад, и с тех пор в iOS SDK делегирование постепенно заменяется на блоки. Не говоря уже про сторонние библиотеки, которые заменили делегирование на замыкания почти для всех основных классов стандартного SDK.


    1. WildGreyPlus Автор
      31.05.2015 21:15

      Objective-C — прекрасный язык, никто не спорит.
      Но в пользовательском коде можно редко увидеть использование блоков вместо делегирования.
      Я не знаю, что останавливает программистов.
      А в Swift это выглядит просто и красиво.
      Сравните простейший пример использования замыканий.

      Objective C

      NSMutableArray *funcs = [[NSMutableArray alloc] init];
      for (int i = 0; i < 10; i++) {
        [funcs addObject:[^ { return i * i; } copy]];
      }
       
      int (^foo)(void) = funcs[3];
      NSLog(@"%d", foo()); // logs "9"
      
      

      Swift

      let funcs = [] + map(0..<10) {i in { i * i }}
      println(funcs[3]()) // prints 9
      

      Мне кажется в Swift использование замыканий вместо делегирования опустит планку для разработчика.


      1. visput
        01.06.2015 02:56

        Я с Вами не соглашусь. То, что в коде, с которым вы сталкивались не используются блоки в качестве делегатов, не значит, что в целом разработчики игнорируют такой способ. Замена делегатов на блоки в стандартном SDK и количество плюсиков у библиотеки, на которую я сослался в предыдущем комментарии, подтверждают мои слова.
        То, что в Swift замыкания описываются более лаконично, чем в Objective-C, нельзя считать причиной того, что Objective-C разработчики предпочитают использовать делегирование вместо блоков. Потому что Swift в целом изначально проектировался как лаконичный язык, большое количество возможностей сократить код замыкания в зависимости от условий (наличие аргументов, наличие возвращаемого типа и т.д) хорошо демонстрирует это.
        Вы же не скажете, что Objective-C разработчики стараются избегать объявлять свойства в классе, а Swift разработчики обожают это делать, потому что в Swift это выглядит просто и красиво:
        Objective C:

        @property (nonatomic, strong) NSMutableString *string;
        
        - (instancetype)init {
            self = [super init];
            if (self) {
                string = [NSMutableString string];
            }
        }
        

        Swift:
        var string = ""
        

        Пример грубый, но я думаю ясно отражает суть мысли.

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

        В Objective-C этот процесс можно сделать таким же простым как и в Swift.


  1. WildGreyPlus Автор
    01.06.2015 10:17

    Да, я соглашусь с вашим замечанием относительно того, что в моей статье закралась неточность относительно использования замыканий (блоков) вместо делегирования в Objective-C.
    Спасибо вам за очень обстоятельное разъяснение и ссылку на библиотеку.
    Я внесла необходимые изменения в статью. Действительно, мой опыт использования замыканий в Objective-C для взаимодействия между View и Сontroller и между различными MVC ограничен.
    В своей статье я вовсе не хочу сравнивать использование замыканий в Objective-C и в Swift, я хочу сказать: " Смотрите, как просто этим пользоваться в Swift. Не нужны никакие дополнительные библиотеки, никакие вспомогательные протоколы и делегаты. Достаточно вложить здравый смысл в простой и понятный синтаксис Swift."
    Пользуясь тем, что есть возможность поговорить с умным собеседником, не могли бы вы для полноты картины привести простой, как на Swift, код на Objective-C для простого примера, указанного в статье, когда есть GraphView и GraphViewController, а GraphView запрашивает данные о зависимости y = f(x).
    Для Swift это выглядит так:
    GraphView

    typealias yFunctionX = ( x: Double) -> Double?
        var yForX: yFunctionX?
    . . . . . . . . . . 
     func drawCurveInRect(bounds: CGRect, origin: CGPoint, pointsPerUnit: CGFloat){
    . . . . . . . . .
            if let y = (self.yForX)?(x: Double ((point.x - origin.x) / scale)) {
    . . . . .
             } 
    }
    

    GraphViewController
    . . . . . . . . . .
      @IBOutlet weak var graphView: GraphView! { didSet {
    graphView.yForX = { [unowned self](x:Double)  in
                    self.brain.setVariable("M", value: Double (x))
                    return self.brain.evaluate()
                }
    }
    

    Для Objective-C так ...?


    1. visput
      01.06.2015 18:31

      На Objective-C это будет выглядеть вот так (дословный перевод):

      // GraphView
      typedef double (^yFunctionX)(double x);
      .......
      @property (nonatomic, copy) yFunctionX yForX;
      .......
      - (void)drawCurveInRect:(CGRect)bounds origin:(CGPoint)origin pointsPerUnit:(CGFloat)pointsPerUnit {
          if (self.yForX != nil) {
              double y = self.yForX((double)((point.x - origin.x) / scale);
              ......
          }
      }
      
      // GraphViewController
      @property (nonatomic, weak) IBOutlet GraphView *graphView;
      .......
      - (void)setGraphView:(GraphView *)graphView {
          _graphView = graphView
          __weak typeof (self) weakSelf = self;
          _graphView.yForX = ^(double x) {
              [weakSelf.brain setVariable:@"M" value:x];
              return [weakSelf.brain evaluate];
          };
      }
      


      1. WildGreyPlus Автор
        01.06.2015 18:49

        Спасибо большое.
        Действительно все понятно и достаточно кратко в пределах возможностей Objective-C.
        Я думаю, этот вариант стоит попробовать тем, кто программирует на Objective-C и не только.


        1. visput
          01.06.2015 20:46

          Единственный минус использования замыканий/блоков вместо протоколов заключается в том, что компилятор/анализатор не подскажет Вам, если Вы забудете проинициализировать свойство замыкания/блока (в примере было yForX). В случае использования протокола мы получим warning о том, что обязательные методы протокола не реализованы в классе-делегате.


          1. WildGreyPlus Автор
            01.06.2015 21:20

            Для моей реализации в Swift это не является недостатком, так как я намеренно сделала переменную-замыкание yForX Optional, то есть графика может и не быть

                var yForX: yFunctionX?
            

            и использую я ее как Optional при построении графика

                 if let y = (self.yForX)?(x: Double ((point.x - origin.x) / scale)) {
            .  .  .  .  
            

            Название функции заключается в круглые скобки и ставится знак? вопроса для корректного построения цепочки Optionals. В случае, если замыкание -переменная yForX не определена в GraphViewController, то аварийного завершения приложения не будет — просто не построится график.
            В Objective-C нет Optional значений. Там в случае не определения замыкания, приложение закончится аварийно.


            1. visput
              01.06.2015 21:34

              так как я намеренно сделала переменную-замыкание yForX Optional

              В этом то и проблема, yForX не должен быть опциональным, потому что без этого замыкания GraphView не имеет значения, так как она не выполнит своего главного назначения — отобразить график. Указывая здесь optional Вы прячете потенциальный баг в приложении.
              Если бы я делал эту фичу с помощью протокола, я бы сделал этот метод @required, в таком случае ошибка не будет запрятана.


              1. WildGreyPlus Автор
                01.06.2015 21:49

                Почему не имеет значения? На графике есть оси, но могут быть и другие графические элементы, может быть несколько графиков, а построение конкретного графика yForX отдается на откуп пользователю: хочет строит, хочет — нет. Эту ошибку не спрячешь — она сразу себя покажет на графике.
                Но в некоторых случаях, я с вами согласна, наличие замыкания обязательно. Например, если вы удалили функцию в строке в Popover, то вам нужно обязательно синхронизировать это удаление с Моделью в Controller. Здесь замыкание не может быть Optional.


                1. visput
                  01.06.2015 21:57

                  yForX отдается на откуп пользователю: хочет строит, хочет — нет.

                  Согласен, это имеет смысл, о таком варианте я не подумал.


            1. visput
              01.06.2015 21:46

              Дополню, это тоже самое, если бы метод — (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section в UITableViewDataSource протоколе сделать опциональным. И в случае, если этот метод не реализован, то таблица будет отображать пустоту. Это некорректное состояние для таблицы.