Задание 5 Stanford CS 193P Fall 2017. Галерея изображений Image Gallery. Решение обязательных пунктов. Часть 2.

Содержание

В Задании 5 вы должны освоить работу с Table View, Collection View, Scroll View и Text Fields, понять, как работает многопоточность (multithreading) и механизма Drag & Drop

Разработка Задания 5 начинается “с нуля”. Оно не имеет отношения к первым 4-м Заданиям этого семестра.

Текст Домашнего задания на английском языке доступен на  iTunes в пункте “Programming: Project 5: Image Gallery″. На русском языке вы можете скачать Задание 5 здесь: Задание 5.pdf.

Для решения Задания 5 необходимо ознакомиться с Лекцией 7, Лекцией 11Лекцией 12 и началом Лекции 13.

Центральной частью этого Задания 5 является коллекция изображений Collection View, которая моделирует Галерею Изображений Image Gallery. Необходимо обеспечить ее работоспособность, настроить ее параметры для отображения изображений в удобной и визуально привлекательной форме, подключить определенные жесты для операций над ее элементами, а также наделить ее механизмом Drag & Drop, который позволит добавлять изображения из других приложений, например, поисковой системы Google, и избавляться от ненужных элементов, кидая их в «Мусорный контейнер». В приложении предполагается создание целой серии таких тематических Галерей Изображений, каждой из которых будет дано имя, а список имен всех Галерей размещен в таблице Table View, которую тоже можно будет редактировать.

Логически выполнение обязательных пунктов Задания 5 распадается на две части: первая часть ( ей был посвящен пост «Задание 5 Stanford CS 193P Fall 2017. Галерея изображений Image Gallery. Решение обязательных пунктов. Часть 1.») будет обеспечивать работу одной Галереи Изображений Image Gallery и будет использовать исключительно коллекцию Collection View, а вторая часть будет обеспечивать работу со списком имен Галерей Изображений с помощью таблицы Table View, функционирование которой мы отработаем отдельно, а затем подстыкуем к ней коллекцию Collection View, уже настроенную под Галерею изображений Image Gallery.

Вторая часть будет представлена в этом посте.

Мое решение обязательных пунктов Задания 5 находится на Github для iOS 11 и на Github для iOS 12  в папке ImageGallery_V. Задание очень большое и сложное, поэтому я разместила код для нескольких последовательных этапов его выполнения, что позволит вам проверять ваш код на разных стадиях выполнения Задания 5:

  1. ImageGalleryOnly — работает только коллекция изображений Сollection View
  2. ImageGalleryRequiedTable — работает только таблица имен Table View
  3. ImageGalleryRequiedTwoSeguesSplit View Controller с двумя Segues для разных ПРОТОТИПОВ
  4. ImageGalleryRequiedGenericSegueSplit View Controller с одним Segue и ручным «переездом»
  5. ImageGalleryRequiedNoSegueSplit View Controller без Segue для iPad (наиболее комфортный для пользователя), это ОКОНЧАТЕЛЬНЫЙ ВАРИАНТ выполнения обязательных пунктов Задания 5.

Пункт 10 обязательный

Добавьте Split View Controller в ваше приложение, у которого Detail — это коллекция Collection View, а Master — это таблица Table View, которая позволяет пользователю выбирать Галерею изображений (Image Gallery) по имени (то есть эта таблица Table View полна имен Галерей изображений (Image Galleries). Если вы коснетесь ячейки в Table View, откроется Галерея изображений (Image Gallery) в коллекции  Collection View, которая описана выше, показывая все ее изображения и разрешая пользователю “бросить” Drop в нее еще больше изображений).

Мы не будем сразу же добавлять Split View Controller в наше приложение, а создадим сначала Master — таблицу Table View, которая позволит пользователю выбирать Галерею изображений (Image Gallery) по имени, а также редактировать это имя.  Detail —  это коллекция Collection View, получение которой уже было описано в предыдущем посте.

Я вытягиваю Table View Controller из Палитры Объектов на storyboard и вставляю эту таблицу в Navigation Controller:

Перенесем стрелочку «старта приложения» с Navigation Controller, обслуживающего Collection View, на Navigation Controller, обслуживающего Table View:

Пункты 11, 12, 13 обязательные

11. Реализуйте жест swipe для уничтожения Галерей изображений (Image Galleries), хотя  их уничтожение просто перемещает их в другую секцию (section) в вашей таблице с именем  “Recently Deleted” (так что таблица Table View будет иметь две секции, одна — без заголовка, а другая — с заголовком “Recently Deleted”. Уничтожение Галереи изображений (Image Gallery) из секции Recently Deleted удаляет эту Галерею изображений из таблицы навсегда.

12. Реализуйте жест swipe (в другом направлении) для операции НЕ УДАЛЕНИЯ (undelete) Галереи изображений (Image Gallery) из  секции Recently Deleted (то есть перемещения ее обратно в другую секцию). Не позволяйте пользователю открывать Галерею изображений (Image Gallery) из секции Recently Deleted без выполнения сначала операции НЕ УДАЛЕНИЯ (undelete). Смотри подсказки о том, как сделать нужный UI для такого рода  “жеста swipe >в другом направлении”.

13. Позвольте пользователям выполнять двойной жест tap на Галерее изображений (Image Galleries>) в вашей таблице Table View для того, чтобы начать редактировать ее имя через текстовое поле UITextField в строке таблицы (то есть редактирование имени “по месту”).

Мы предусмотрим два ПРОТОТИПА для ячеек : один, Gallery Cell — пользовательская (Custom) ячейка с редактируемым текстовым полем Name Text Field для изменения имени Галереи изображений «по месту», другой, Title CellBasic ячейка с меткой Title для размещения имени Галереи изображений в секции Recently Deleted, имя в этой секции нельзя редактировать. 

Для пользовательской (Custom) ячейки Gallery Cell создадим класс GalleryTableViewCell, который является subclass класса UITableViewCell  с Outlet nameTextField к текстовому полю, делегатом UITextFieldDelegate и возвращающим замыканием resignationHandler:

Мы должны указать пользовательский subclass GalleryTableViewCell для этого прототипа Gallery Cell в Инспекторе Идентичности:

Ячейка прототипа Title Cell имеет Basic Style и ей не требуется пользовательский subclass:

Воспользуемся подсказкой № 9 для определения свойств текстового поля:

У вас нет необходимости выполнять трюк “переключения на другую ячейку cell, который мы проделали в EmojiArt, для того, чтобы отредактировать имя Галереи изображений (Image Gallery) в вашей таблице Table View. Просто все время используйте текстовое поле UITextField для рисования имени и включите опцию isEnabled для установления режима редактирования этого текстового поля, если необходимо, основываясь на двойном tap жесте.

На storyboard установим свойство isEnabled текстового поля UITextField в false. Мы будем включать его, используя двойной tap жест.

Это очень простая ячейка cell, у которой всего один Outlet, текстовое поле nameTextField. Заметьте, как только мой Outlet установлен системой, я немедленно делаю себя, self, делегатом delegate текстового поля nameTextField и  добавляю в текстовое поле двойной tap жест для включения способности редактирования в методе beginEditing ():

Я реализую только один метод делегата UITextFieldDelegate, это textfieldShouldReturn, и что он делает?

Как только я нажму на моей клавиатуре клавишу Return, он вызывает метод resignsFirstResponder(), который останавливает использование клавиатуры, и она исчезает с экрана. Если вы не напишите этот очень небольшой код, то при нажатии клавиши Return клавиатура останется на экране и курсор будет “мигать”. Наш код заставит клавиатуру “уйти” с экрана при нажатии клавиши Return и сделает текстовое поле нередактируемым:

Мы также будем использовать метод делегата delegate, который называется textFieldDidEndEditing:

Когда будет вызван этот метод, я хотела бы “говорить” с моей таблицей Table View и сказать ей, что нужно изменить в ней имя Галереи Изображений на то, которое находится в моем текстовом поле nameTextField. Но как я буду говорить с таблицей Table View из класса GalleryTableViewCell пользовательской ячейки? У ячейки UITableViewCell нет указателя на свою таблицу Table View, нет такого API у класса UITableViewCell. Это интересная задача, и есть очень легкий путь ее решения это с помощью замыканий (closures).

Я создаю переменную var, public var в моей ячейке GalleryTableViewCell с именем resignationHandler. Эта переменная var будет замыканием, то есть функцией, у которой нет аргументов и которая ничего не возвращает. На самом деле я сделаю это замыканиеOptional функцией, так что она может равняться nil и ее не нужно устанавливать:

Когда редактирование текстового поля nameTextField завершается, я вызываю эту функцию resignationHandler? ():

Так как она может равняться nil, то я использую цепочку Optional, чтобы вызвать ее. Итак, я просто вызываю эту функцию. Теперь любой, кто заинтересован в том, когда мое текстовое поле заканчивает редактирование, может установить это замыкание во что-то конкретное.

И именно это мы будем делать в нашем Table View Controller, для которого создадим пользовательский subclass класса UITableViewController для управления этой таблицей. Я иду в меню File -> New -> File и  использую Cocoa Touch Сlass

… и создам subclass класса UITableViewController, который назову GalleriesTableViewController:

Давайте выберем Table View Controller на storyboard и в Инспекторе Идентичности установим новый класс GalleriesTableViewController.

Теперь я могу делать все эти “волшебные” вещи, связанные с делегатами delegate и dataSource:

Фактически, если мы взглянем на код этого класса, то увидим, что он дает нам множество закомментированного кода, который работает с таблицей Table View, потому что я создала мой класс как subclass класса UITableViewController, а он знает, как обращаться с таблицами и какие методы им нужны.

Я собираюсь удалить некоторые методы “жизненного цикла” нашего View Controller, которые мы не будем использовать,  а оставшиеся методы numberOfSections, numberOfRows, cellForRowAt являются наиболее важными методами для таблицы Table View, это методы UITableViewDataSource для получения данных.

Мы хотим их реализовать, чтобы заставить работать таблицу Table View.

Всякий раз, когда мы имеем новый MVC, мы должны думать о том, а какая у него Модель? И здесь воспользуемся подсказками № 15 и № 16 Задания 5:

15. Одна из наиболее хитроумных частей этого Задания в действительности заключается в тестировании ваших навыков в использовании MVC, которые должны проявиться в возможности “переезда” (segue) от MVC таблицы Table View к MVC вашей коллекции Collection View. Моделью MVC вашей коллекции Collection View по существу является Галерея изображений (Image Gallery). Моделью MVC вашей таблицы Table View является  список этих Галерей изображений (Image Galleries). Единственная загвоздка состоит в том, что оба MVCs могут редактировать Галерею изображений (Image Gallery) (потому что MVC вашей таблицы Table View может изменять имя Галереи изображений (Image Gallery)). Просто убедитесь, что вы придумали простую структуру данных, которая позволяет вашему MVC таблицы Table View “отдать” Галерею изображений (Image Gallery) вашему MVC коллекции Collection View и оставаться при этом способной изменять имя Галереи изображений (Image Gallery).

16. Ваша внутренняя структура данных для Галереи изображений (Image Gallery) является очень простой (это список URLs и их Aspect ratios). Не перемудрите с этой частью этого Задания.

Моделью изображения является структура struct ImageModel, содержащая URL url и Aspect ratio aspectRatioМоделью Галереи является структура struct ImageGallery, содержащая имя Галереи Изображений name и набор изображений images, представленных массивом [ImageModel].

Наша таблица должна отображать множество Галерей в разных секциях, поэтому Моделью множество Галерей imageGalleries для MVC таблицы Table View будет двумерный массив Галерей [[ImageGallery]]: внешний массив — это секции, а внутренний — галереи в секциии:

Итак, переменная var imageGalleries— это моя Модель. И наша таблица должна показать мою Модель.

У меня будет столько секций sections, сколько внешних массивов содержится в массиве [[ImageGallery]], и я хочу избавиться здесь от предупреждений, потому что я точно знаю, сколько будет секций:

Следующий метод — число строк в секции numberOfRowsInSection. Мне это точно известно исходя из моей Модели :

Затем у нас есть метод cellForRowAt, в котором я должна использовать метод dequeueReusableCell для получения повторно-используемой ячейки определенного ПРОТОТИПА. У нас два ПРОТОТИПА: один — пользовательский Gallery Cell, управляемый классом GalleryTableViewCell и содержащий текстовое поле nameTextField для редактирования имени Галереи Изображений, а другой — стандартный Title Cell стиля Basic, содержащий метку title для отображения имени Галереи в секции Recently Deleted. Пока мы не добавили функциональности, связанной с редактированием текстового поля nameTextField, в метод cellForRowAt, поэтому код будет выглядеть практически одинаковым для обоих ПРОТОТИПОВ:

Вспомогательная private функция galleryName обеспечивает получение имени Галереи Изображений из нашей Модели по заданному indexPath:

Для ПРОТОТИПА Gallery Cell мы добавляем возможность редактирования текстового поля с помощью замыкания resignationHandler, вызываемого в классе GalleryTableViewCell :

В замыкании resignationHandler мы присваиваем новое имя name, полученное из текстового поля, Галереи Изображений, находящейся в  нашей Модели по соответствующему indexPath: в соответствуещей секции  indexPath.section и в соответствующей строке indexPath.row этой секции, и перезагружаем таблицу tableView с помощью метода reloadData(). У меня не так много данных, так что не будет слишком затратным перезагрузка всей таблицы.

Мы должны с чего-то начать, пусть у нас будут 3 Галереи Изображений с именами “Gallery 1”, “Gallery 2” и “Gallery 3” в основной секции и “Галерея 66” в секции с заголовком “Recently Deleted”. Это наша тестовая Модель, и мы сгенерируем ее в методе «жизненного» цикла viewDidLoad:

Для того, чтобы увидеть различные секции в таблице Table View, создадим заголовки для секций с помощью метода titleForHeaderInSection, он возвращает строку String?, которая и будет использована в качестве заголовка. Ничего не может быть легче.

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

В результате на экране мы получим таблицу имен Галерей Изображений :

Мы видим, что “Gallery 1”, “Gallery 2” и “Gallery 3” расположены в текстовых полях в основной секции имен Галерей Изображения, а  “Галерея 66” представлена меткой и находится в секции «Resent Deleted«. Просто пока заметим, что никакая ячейка таблицы не выделена и не подсвечивается как текущая. Для того, чтобы как-то ориентироваться, на какой строке таблицы мы находимся, давайте вызовем в методе «жизненного цикла» нашего Controller viewDidApper метод таблицы tableView, который судя по названию, выбирает строку таблицы Table View, заданную indexPath :

В результате мы получим подсветку первой строки таблицы:

Отметим сразу, что вызов метода tableView.selectRow(at: IndexPath,animated: Bool,scrollPosition: UITableViewScrollPosition) не приводит к получению делегатом delegate таблицы Table View ни сообщений tableView(_:willSelectRowAt:), ни сообщений tableView(_:didSelectRowAt:), а также не посылает уведомлений  UITableViewSelectionDidChange наблюдателям. Другими словами, этот метод только подсвечивает ту или иную строку таблицы Table View. Реальный выбор строки, который приводит к посылке сообщений tableView(_:willSelectRowAt:) или  tableView(_:didSelectRowAt:), делает пользователь, когда кликает на ней. Нам пригодится эта информация в дальнейшем, если мы будем осуществлять синхронизацию нашей таблицы Table View с коллекцией изображений Collection View.

Идем дальше и реализуем обязательный пункт 11 полностью.

Пункт 11 обязательный

Реализуйте жест swipe для уничтожения Галерей изображений (Image Galleries), хотя  их уничтожение просто перемещает их в другую секцию (section) в вашей таблице с именем  “Recently Deleted” (так что таблица Table View будет иметь две секции, одна — без заголовка, а другая — с заголовком “Recently Deleted”. Уничтожение Галереи изображений (Image Gallery) из секции Recently Deleted удаляет эту Галерею изображений из таблицы навсегда.

Для того, чтобы реализовать swipe-to-delete возможность таблицы Table Vew, при которой пользователь выполняет горизонтальный жест swipe вдоль строки, чтобы показать кнопку Delete, вы должны выполнить этот метод UITableViewDataSource, в котором editingStyle — это либо .delete, либо .insert. Вы должны передать (commit) эти изменения вашей Модели. Случай .insert — это своего рода UI, обеспечиваемый таблицей UITableView для добавления строк, нам это не нужно, так как у нас будет кнопка “+” на навигационной панели. Так что в части .insert мне ничего не надо делать, я должна работать с .delete ситуацией в обоих секциях — 0 и 1:

Мы удалили Галерею изображений, соответствующую передаваемому параметру indexPath, из массива imageGalleries[0], соответствующего 0-ой основной секции, и вставили в массив imageGalleries[1], соответствующий 1-ой секции с именем Recently Deleted в нашей Модели [[ImageGalleries]]. Мы также удалили строку, соответствующую передаваемому параметру indexPath, из таблицы tableView с помощью метода deleteRows и вставили ее снова с помощью метода insertRows, но уже в другую,1-ую секцию. Все эти действия должны быть абсолютно синхронными. Если вы не обеспечите их абсолютной синхронности, то ваше приложение закончится аварийно и вам сообщат, что число строк в таблице не является подходящим при обновлении строк (“Number of rows in the table did not match…”). Другими словами, вам сообщают, что ваша Модель и ваша таблица tableView не соответствуют друг другу. Очень легко прийти к их рассинхронизации. Поэтому мы воспользовались в этом коде подсказкой № 13:

Подсказка № 13. Каждый раз, когда вы обновляете вашу таблицу с помощью комбинации методов deleteRows или insertRows или moveRows, вы должны делать все эти модификации ВМЕСТЕ или ваше приложение может завершится аварийно,  потому что UI вашей таблицы Table View может временно оказаться НЕ синхронизированным с вашей М>оделью. Вам следует сгруппировать вызовы этих методов ВМЕСТЕ с помощью метода performBatchUpdates. У него также есть completion обработчик, который вы можете использовать, чтобы сделать что-то после завершения регулировки Table View, если это необходимо.

Отметим, что мы не можем использовать метод moveRows, для перемещения ячеек из основной секции 0 в секцию Recent Deleted с номером 1, так как в этом случае ячейка переместится в секцию со своим ПРОТОТИПОМ, который в нашем случае отличается от ПРОТОТИПА ячеек в новой секции. Поэтому мы удаляем строку из секции 0 и вставляем новую строку в секцию 1. Мы использовали метод performBatchUpdates и его completion обработчик, в котором выбрали нужную нам строку в этой секции после соответствующих удалений и вставок.  

Вместо метода selectRow (at:IndexPath, animated: Bool, scrollPosition: UITableViewScrollPosition) таблицы tableView, мы использовали наш  private метод selectRow (at indexPath: IndexPath):

Этом метод может пригодиться нам во многих действиях с таблицей Table View. В этом private методе мы сначала определяем, есть ли вообще в заданной секции indexPath.section какие-либо строки, а затем выполняем уже знакомый нам метод tableView.selectRow (at:, animated: , scrollPosition:) таблицы Table View, который обрамляем таймером Timer так, чтобы иметь возможность  с помощью задержки timeDelay отсрочить выбор нужной строки до момента завершения всех анимаций. Задержка timeDelay по умолчанию равна 0.0, и если у вас нет задержки, то последний параметр after при вызове private метода selectRow (at:, after:) можно не задавать. При удалении строки таблицы Table View из основной секции 0 мы используем при вызове метода selectRow (at:, after:)задержку after: 0.3, выбранную экспериментальным путем. При удалении строки таблицы <strong»>Table View из секции1 — Recent Deleted — задержка не нужна и мы вызываем метода selectRow (at:) без параметра after:.

Теперь мы можем удалить “Gallery 1” с помощью кнопки Delete:

… и она окажется в секции Recently Deleted:

Если я использую кнопку Delete в секции Recently Deleted для “Gallery 1”:

…то она удалится навсегда, а указатель строки встанет на 1-ую строку в основной секции:

Пункт 12 обязательный

Реализуйте жест swipe (в другом направлении) для операции НЕ УДАЛЕНИЯ (undelete) Галереи изображений (Image Gallery) из  секции Recently Deleted (то есть перемещения ее обратно в другую секцию). Не позволяйте пользователю открывать Галерею изображений (Image Gallery) из секции Recently Deleted без выполнения сначала операции НЕ УДАЛЕНИЯ (undelete). Смотри подсказки о том, как сделать нужный UI для такого рода  “жеста swipe в другом направлении”.

Swipe actions являются новыми в iOS 11, и их можно теперь задавать на лидирующей (leading) или хвостовой (trailing) сторонах строки таблицы Table View и включать туда соответствующие изображения. Для того, чтобы реализовать “жест swipe в другом направлении” в нашей задаче мы воспользуемся методом leadingSwipeActionsConfigurationForRowAtделегата таблицы, в котором создадим Swipe действие undelete с помощью инициализатора UIContextualAction  и замыкания handler по возврату ячейки из секции 1 в конец секции 0:

Для удаления строки из секции 1 и вставки ее в секцию мы также, как и при обратной операции, использовали метод performBatchUpdates и его completion обработчик, в котором выбрали нужную нам строку в секции 0после соответствующих удалений и вставок с помощью нашего private метода selectRow (at:, after:)  с задержкой after: 0.5, выбранную экспериментальным путем. Почему-то эта операция оказалась для метода selectRow (at:, after:) наиболее тяжелой и задержку пришлось увеличить до 0.5.

Теперь мы можем выполнить “жест swipe в другом направлении” (вправо) и у нас появится возможность вернуть недавно удаленную  ячейку таблицы Table View обратно в основную секцию 0 с помощью «действия» Undelete:

Недавно удаленная ячейка вернется в основную секцию 0, но не на прежнюю позицию, которую мы уже потеряли, а на последнюю:

Мы научились удалять Галереи Изображений нашего списка, отображаемого в таблице Table View.

Теперь давайте также, как и в Лекции 11, научим наше приложение создавать новые Галереи Изображений с помощью правой кнопки «+» на навигационной панели. Для этого перетянем кнопку  Bar Button Item  из Палитры Объектов на навигационную панель:

Итак, мы получили нужную кнопку на навигационной панели и можем ее инспектировать и устанавливать в поле System item заранее определенные кнопки: например, кнопка Add,  которая превращается в плюс «+«…

Выполним CTRL-перетягивание от этой кнопки в код, чтобы создать Action.

Этот Action я назову его newGallery, потому что именно это будет делать кнопка  “+” — создавать новую Галерею Изображений Image Gallery

Теперь, когда мы подсоединили эту кнопку, мы можем начать писать код прямо здесь и создать новую Галерею Изображений.

Я использую оператор += для добавления другого массива к массиву Галерей Изображений в 0-ой секции imageGalleries[0]. Добавляемый массив будет состоять из единственной Галереи Изображений с именем “New Gallery”, именно так я назову свою новую Галерею Изображений. Но что, если у меня уже есть Галерею Изображений с именем “New Gallery”? Я не хочу, чтобы у меня было две Галереи Изображений с одним и тем же именем “New Gallery”, поэтому я использую эту небольшую вспомогательную функцию madeUnique(withRespectTo otherStrings: [String]), которую создал профессор. Она берет другой массив otherStrings, просматривает этот массив и убеждается в том, что создает что-то уникальное по отношению к этому массиву. В нашем случае это будет “New Gallery 1”, “New Gallery 2”, “New Gallery 3” и т.д. До тех пор, пока не подберет что-то уникальное. И я буду использовать функцию madeUnique(withRespectTo:) с массивом имен Галерей Изображений, полученным с помощью функций flatMap и map:

Но этого НЕДОСТАТОЧНО. Я изменила мою Модель в этом коде и должна выполнить метод tableView.reloadData (), чтобы синхронизировать ее с данными.

У меня не предполагается слишком много данных, так что не будет слишком затратным перезагрузка всей таблицы. И опять, для выбора нужной нам строки в секции 0после соответствующей вставки, мы вызываем наш private метод selectRow (at:) без какой-либо задержки after: .

Давайте запустим наше приложение. Кликаем на “+” и получаем “New Gallery“:

Еще кликаем на “+”,  “+”,  “+”,  “+”,  “+”:

Мы слишком увлеклись, нам не нужно так много. Давайте попробуем удалить ненужные строки. Я хочу использовать предусмотренный для этого жест swipe:

Если мы повторим это для всех новых Галерей Изображений, то все они окажутся в секции недавно удаленных Recently Deleted :

Если Галерея Изображений находится в секции Recently Deleted,  то с помощью swipe вправо…

…мы можем вернуть эту Галерею в основной список Галерей Изображений:

Мы можем удалить оставшиеся Галереи New GalleryNew Gallery 1New Gallery 2New Gallery 3 и New Gallery 4 из секции НАВСЕГДА с помощью swipe влево и кнопки Delete

Галерея Изображений Gallery 4 исчезает НАВСЕГДА:

То же самое с остальными New Gallery. В результате получим следующий список Галерей Изображений;

После всех манипуляций курсор установился на первой строке в основной секции 0и это очень ВАЖНО. Потому что в будущем мы захотим держать таблицу Table View в синхронном состоянии с нашей коллекцией изображений Collection View. Для этого мы должны выполнить Дополнительный пункт 3 Задания 5:

Пункт 3 дополнительный

Все время держите таблицу Table View в синхронном состоянии с вашей коллекцией Collection View<. Это может быть немного более хитроумно, чем кажется. По существу, вам следует ВСЕГДА иметь выбор строки (selection) в таблице Table View, и эта выбранная строка должна ВСЕГДА соответствовать тому, что показывается в коллекции Collection View. Таблица Table View может быть весьма привередливой относительно выбора строк с помощью selectRow (at:). Если она заканчивает анимацию, например, то выбор строки может не состояться. Возможно, вам следует использовать таймер Timer для того, чтобы задержать немного выбор строки. Конечно, вы также должны использовать performSegue, когда выбираете строку от имени пользователя. Есть также ошибка выбора строки при первом появлении таблицы Table View. Возможно, вам следует ждать по крайней мере до viewDidAppear, чтобы сделать это.

Выполнение этого пункта состоит из двух частей: сначала мы должны обеспечить выбор в таблице Table View определенной строки indexPath.row в определенной секции indexPath.section, а потом обеспечить показ коллекции изображений, соответствующей этой ячейки, в MVC с коллекцией изображений Collection View.

Сейчас, когда в нашем распоряжении находится только таблица Table View, не связанная с коллекцией Collection View, мы можем обеспечить и проверить только ПЕРВУЮ ЧАСТЬ — выбор нужной строки таблицы Table View с помощью нашего private метода selectRow (at:, after: ) с соответствующей задержкой after, если это необходимо. ВТОРАЯ ЧАСТЬ — синхронизация ячейки таблицы Table View с коллекцией Collection View, отображающей коллекцию изображений,  и мы рассмотрим позже, когда в нашем приложении появится Split View Controller.

Хотя этого и не требуется в обязательных пунктах Задания 5, для правильной работы приложения, мы должны сделать выбор строки в таблице с помощью private метода  selectRow (at:, after:) при старте приложения и тогда, когда мы удаляем Галерею Изображений, которая в данный момент показывается в коллекции Collection View, и тогда, когда изменяем имя Галереи Изображений name, и тогда, когда возвращаем Галерею Изображений назад в основную секцию с помощью “жеста swipe в другом направлении”. 

Поэтому мы вызываем метод selectRow (at:) при старте приложения в методе viewDidAppear:

Мы вызываем метод selectRow (at: ) при добавлении новой Галереи Изображений в @IBAction func newGallery :

Мы вызываем метод selectRow (at: ) в возвращаемом замыкании resignationHandler при завершении редактирования имени name Галереи Изображений в строке таблицы:

Мы вызываем метод selectRow (at:, after: ) при выполнении жеста swipe влево при перемещении строки из основной секции 0 в секцию Resently Deleted и удалении НАВСЕГДА из секции 1:

Мы вызываем метод selectRow (at indexPath: IndexPath) при выполнении жеста swipe вправо при обратном перемещении строки из секции Resently Deleted в основную секцию 0:

На этом мы заканчиваем автономную работу с Table View. Код для случая автономной работы с таблицей Table View располагается на Github   для iOS 11 и на Github для iOS 12 в папке ImageGalleryRequiredTable.

Переходим к обязательному пункту 10 :

Пункт 10 обязательный

Добавьте Split View Controller в ваше приложение, у которого Detail — это коллекция Collection View, а Master — это таблица Table View, которая позволяет пользователю выбирать Галерею изображений (Image Gallery) по имени (то есть эта таблица Table Viewполна имен Галерей изображений (Image Galleries). Если вы коснетесь ячейки в Table View, откроется Галерея изображений (Image Gallery) в коллекции Collection View, которая описана выше, показывая все ее изображения и разрешая пользователю “бросить” Drop в нее еще больше изображений).

Я иду на storyboard и размещаю там Split View Controller. Профессор всегда рекомендует существенно уменьшать масштаб при размещении Split View Controller на storyboard, потому что он “тащит” за собой много вещей по умолчанию. Найдем в Палитре Объектов Split View Controller и вытащим его на storyboard со всем, что приходит с ним по умолчанию.

Я удаляю все, что пришло по умолчанию, оставляю только Split View Controller и переношу маленькую стрелочку с Navigation Controller на Split View Controller:

Этот Navigation Controller вместе с вставленным в него Galeries Table View Contriller будет, очевидно, моим Master. А Detail, понятное дело, будет соответствовать Галерее Изображений Image Gallery Collection View, с которой я хочу работать. Так что я делаю CTRL-перетягивание от Split View Controller к Navigation Controller, в который вставляется Galleries Table View Controller, и устанавливаю его как Master:.

В результате получаем взаимосвязь Master:

Далее я делаю CTRL-перетягивание от Split View Controller к Navigation Controller, в который вставляется Image Gallery Collection View Controller, и устанавливаю его как Detail.

В результате получаем Detail:

Запустим наше приложение в обычном однозадачном режиме.

Мы видим слева таблицу Table View, которая не является главной в нашем UI, и ее постоянное присутствие действительно раздражает пользователя, не давая ему возможности сфокусироваться на коллекции Collection View, содержащий изображения, “перетянутые” на нее с помощью механизма Drag & Drop. Это происходит потому, что в ландшафтном режиме на iPad Split View Controller показывает свои Мaster и Detail бок о бок (side by side). Правда, если я перейду  в многозадачный режим, расположив справа Safari, то таблица скроется…

… и ее можно будет вернуть на экран с помощью жеста swipe.

Таким образом Split View Controller адаптирует расположение своих Мaster и Detail в условиях многозадачного режима, когда значительная часть экрана справа занята Safari.

Это происходит потому, что в самом Split View Controller установлен предпочтительный режим показа preferredDisplayMode, соответствующий значению по умолчанию.automatic. Это означает, что сам Split View Controller автоматически решает, какой режим показа ему кажется наиболее подходящим для данного устройства и размера текущего приложения.

  • В многозадачном режиме Split View наш Split View Controller адаптируется в ландшафтном режиме так, что показывает только Detail, а Master заставляет появляться поверх Detail и исчезать с помощью жеста swipe вправо и влево.
  • В многозадачном режиме Slide Over наш UISplitViewController никак не адаптируется в ландшафтном режиме, он ведет себя обычным способом: Master и Detail одновременно находятся на экране.

В подсказке № 17 к Заданию 5 профессор хочет избавиться от назойливой таблицы не на уровне адаптации к многозадачному режиму, а на уровне переменной предпочтительного режима показа preferredDisplayMode, так, чтобы ВСЕГДА таблица в Master появлялась поверх Detail и скрывалась помощью жеста swipe:

17. Возможно, вы захотите установить свойство preferredDisplayMode вашего splitViewController в .primaryOverlay. Это потому, что фокус вашего приложения — это коллекция Collection View, и вы не захотите, чтобы таблица Table View находилась на экране слишком много (только когда вам необходимо переключиться на другую Галерею изображений (Image Gallery)). Так как этот режим может переустанавливаться iOS при изменении расположения (layout), то наилучшее место для установки режима .primaryOverlay — это метод viewWillLayoutSubviews, но будьте внимательны и проверяйте, что если он уже установлен, потому что повторная его установка вызовет опять изменение расположения (layout), и вы попадете в БЕСКОНЕЧНЫЙ цикл! Также вы можете добавить кнопку на навигационную панель в вашем Detail, которая вызовет появление Master (то есть сделает то же самое, что делает пользователь, выполняя жест swipe слева направо). Для этого можно использовать код в методе viewDidLoad вашего Detail
navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem

Я иду в класс GalleriesTableViewController, управляющей моей таблицей c именами Галерей Изображений, это то, что мы хотим убрать с экрана скольжением с помощью жеста swipe. Я хочу установить переменную preferredDisplayMode совершенно в необычном месте: в методе “жизненного цикла” viewWillLayoutSubviews. Вы можете подумать: “ Вот это да! Похоже viewWillLayoutSubviews — это очень странное место для размещения этого кода!” Причина, по которой я хочу устанавливать переменную preferredDisplayMode именно в этом месте, заключается в том, что при изменении расположения Split View Controller часто переменная preferredDisplayMode переустанавливается заново. Если я хочу принудительно держать Split View Controller в таком режиме, когда Master будет появляться поверх Detail скольжением, то я должна присвоить переменной preferredDisplayMode нужное мне значение, но нужно также быть очень внимательным, делая это, потому что установка переменной preferredDisplayMode может вызвать повторное изменение расположения UI элементов (relayout). Мы не хотим втянуться в бесконечный цикл вызова viewWillLayoutSubviews, в котором устанавливается переменная preferredDisplayMode, это вызывает изменение расположения и вызывается viewWillLayoutSubviews, я опять возвращаюсь в исходную точку и опять устанавливаю переменную preferredDisplayMode. Поэтому я буду использовать оператор if:

Если переменная preferredDisplayMode моего splitViewController не равна тому значению, которое я хочу, а здесь может быть несколько вариантов значения: .primaryOverlay, .primaryHidden, .automatic, .allVisible (когда оба, Master и Detail, находятся на экране). Но мне нужно значение .primaryOverlay. Так вот, если preferredDisplayMode не равен .primaryOverlay, то я устанавливаю переменной preferredDisplayMode моего splitViewController значение, равное .primaryOverlay. Этот код будет выполняться каждый раз при вращении или по каким-то другим причинам изменения расположения Split View, принудительно поддерживая режим показа splitViewController равным .primaryOverlay, потому что я всегда хочу, чтобы таблица Table View появлялась поверх моего Collection View. Кроме того, я хочу иметь возможность в любое время отодвинуть таблицу в сторону или заставить ее появиться с помощью жеста swipe.

Запускаем наше приложение, и видим, что независимо от того, имеем ли мы многозадачный режим..

или нет…

… таблица со списком Галерей Изображений появляется, но ее легко убрать, просто кликнув где-нибудь на Collection View :

Таблица Table View уходит, но с помощью жеста swipe я могу вызывать ее на экран и убирать с экрана, вызывать и убирать. 

Кроме того, мы добавили кнопку на навигационную панель в вашем Detail, ImageGalleryCollectionViewController. Эта кнопка в виде знака «<» вызовет появление Master (то есть сделает то же самое, что делает пользователь, выполняя жест swipe слева направо). Для этого мы разместили  код в методе viewDidLoad вашего Detail — ImageGalleryCollectionViewController:

Пока в нашем Split View Controller таблица GalleriesTableViewController с именами Галерей Изображений и коллекция изображений ImageGalleryCollectionViewController для отдельно взятой Галереи никак не взаимодействуют. Их взаимодействие мы можем обеспечить с помощью  Segue, но это не будет АВТОМАТИЧЕСКИЙ «переезд» на MVC c коллекцией изображений ImageGalleryCollectionViewController, так как в Задании 5 есть условие, описанное в обязательном пункте # 12:

12. … Не позволяйте пользователю открывать Галерею изображений (Image Gallery) из секции Recently Deleted без выполнения сначала операции НЕ УДАЛЕНИЯ (undelete)…

Нам предлагают подсказку № 10 для выполнения такого «условного переезда» (Segue):

Подсказка № 10. Вы можете предотвратить “переезд” (segueing) из Recently Deleted строк либо путем использования различных прототипов для них, либо путем реализации метода shouldPerformSegue(withIdentifier:).

У нас два различных прототипа для представления ячеек: в основной секции и в секции Recently Deleted:

Давайте сначала создадим Segue от прототипа, соответствующего основной секции, к Navigation Controller нашего Detail и дадим ему идентификатор «Show Gallery» :

Реализуем метод подготовки prepare (for segue: UIStoryboardSegue, sender: Any?) в Controller GalleriesTableViewController таблицы с именами  для Segue с идентификатором «Show Gallery«:

Мы определяем indexPath выбранной ячейки cell:

if let cell = sender as? GalleryTableViewCell,
let indexPath = tableView.indexPath(for: cell) {

… получаем ivc Detail нашего Split View Controller:

if let ivc = segue.destination.contents as? ImageGalleryCollectionViewController {

… устанавливаем его коллекцию ivc.imageCollection равной коллекции Галереи Изображений, находящейся в заданной секции indexPath.section и в заданной строке indexPath.row в нашей таблице Table View:

ivc.imageCollection = imageGalleries[indexPath.section][indexPath.row].images

…устанавливаем заголовок Detail равным имени соответствующей Галереи Изображений :

ivc.title = imageGalleries[indexPath.section][indexPath.row].name

Для тестирования Split View Controller мы добавим в  Галерею с именем «Gallery 1» несколько изображений:

Запускаем наше приложение и смотрим, что будет.

И у нас возникает сразу несколько проблем.

Во-первых, хотя первая строка таблицы подсвечивается, она не выбирается, нам нужно кликнуть на ней, чтобы сработал наш Segue (мы займемся автоматизацией этого процесса немного позже):

Кликнув на первой строке, мы увидим, что на нашей коллекции появился заголовок «Gallery 1» и изображения, находящиеся в этой Галерее. Давайте добавим несколько изображений из Google в эту Галерею:

Мы можем вернуться назад, в нашу таблицу, и переименовать Галерею из «Gallery 1» в «sunrise» :

После переименования мы можем кликнуть на коллекции и вернуться назад, в коллекцию, и продолжить добавлять изображения в нашу коллекцию изображений Галереи «sunset«:

С помощью левой кнопки «<» на навигационной панели опять вызовем таблицу со списком Галерей и выберем Галерею «Gallery 2«, естественно, она пустая:

Мы можем начать добавлять в Галерею «Gallery 2» изображения, связанные с тематикой «ocean«:

Мы даже можем переименовать Галерею из «Gallery 2» в «ocean«:

После переименования, в таблице подсвечивается новое имя Галереи — «ocean«, но мы должны кликнуть на новом  имени, чтобы оно появилось как заголовок коллекции:

Имя  «ocean» появилось, но все изображения, которые мы добавили к этой Галерее, исчезли. Более того, если мы вернемся к Галерее с именем «sunrise«, мы и там  не увидим вновь добавленных изображений, там остались только те тестовые изображения, которые были добавлены изначально в Галерею «Gallery 1«, впоследствии переименованной в Галерею «sunrise» :

В чем же дело? А дело в том, что Моделью таблицы Table View является массив структур struct ImageGallery:

var imageGalleries = [[ImageGallery]] ()

… и Моделью коллекции Collection View является массив структур struct ImageModel:

var imageCollection = [ImageModel]()

В Swift массивы структур повсюду, в том числе и между Controllers, передаются по значению, то есть копированием, а точнее используя технологию (COW — copy-on-write), то есть копирование экземпляра массива происходит при изменении массива. Как только мы начнем добавлять изображения в массив структур [ImageModel], мы с каждым новым изображением будем копировать массив структур [ImageModel], и он уже не вернется в экземпляр класса GalleriesTableViewController, который хранит полную Модель списка [[ImageGallery]] всех Галерей с их именами и изображениями:

Можно ли каким-то способом передать изменившийся массив изображений [ImageModel] назад, в экземпляр класса  GalleriesTableViewController, и там обновить массив изображений images для этой Галереи?

Можно. То есть фактически, нам нужно «разделять» структуру» struct ImageGallery между двумя MVC. Обычно это делают с помощью «боксинга» (boxing), то есть размещения структуры struct в классе final class :

В этом случае box1 и box2 ссылаются на одну и ту же Галерею imageGallery, и достаточно изменить имя name Галереи, находящейся в box2, как мы увидим, что изменилось и имя name Галереи, находящейся в box1. Именно это нам и нужно.

 

Но это в теории. Однако наш случай гораздо проще, нам достаточно сделать ImageGallery не структурой struct, а классом class:

Для того, чтобы обосновать правильность этого решения, давайте сделаем небольшое отступление и рассмотрим отличие Value Type структур данных и  Reference Types структур данных.

Дело в том, что начиная с WWDC 2015 Apple настойчиво рекомендует чаще использовать Value Type структуры данных, к которым относятся:

  • перечисления enum
  • кортежи (Tuple)
  • примитивы (Int, Double, Bool и т.д.)
  • коллекции Collections (Array, String, Dictionary, Set)

… в противоположность Reference Types, к которым относятся:

  • классы Class
  • все, что происходит от NSObject
  • функции Function
  • замыкания Closure

Базовое отличие Value Type состоит в том, что они копируются — это происходит при присвоении, инициализации и передаче аргумента функции. Каждый раз в этом случае создается независимый экземпляр Value Type со своей собственной уникальной копией своих данных.

Если вы копируете обычную строку String, то у вас не будет никаких проблем с производительностью. Вы начинаете испытывать некоторые сложности, если ваш массив Array содержит тысячи элементов и вы копируете его повсюду в вашем приложении. По этой причине некоторые Value Types типа Strings или Arrays неявно держат свои элементы в «куче». Таким образом, они являются Value Types с хранением в виде Reference Types и используют механизм COW (copy-on- write) для сохранения производительности и избавления от бесполезных копий.

 Value семантика уничтожает «изменчивость» (mutation), удаляет ненамеренное «совместное использование» состояния (state) и побочные эффекты, является thread safe, так как никогда не возникает никаких гонок «race conditions«, блокировок (locks), deadlocks или каких-либо других сложностей, связанных с многопоточной синхронизацией. Кроме того, Value Types обеспечивают лучшие показатели производительности, чем Reference Types.

Но все это, так сказать, общая теория. А когда доходит дело до конкретных случаев, она начинает рушится, как показано в статье с амбиционным названием «Stop Using Structs!». («Прекратите использовать структуры structs!»).

Value Types могут обладать всеми своими преимуществами только в том случае, если они остаются «чисто Value Types. В том случае, если Value Type содержит Reference Type или Value Type c Reference Type хранением (Strings, Arraysи т.д.), прекрасный Мир Value Types может разрушиться и остаться позади Мира Reference Type. В статье приводится схема, которая должна приниматься во внимание при выборе между Value Types и Reference Types:

У нас был как раз тот случай, когда структура struct ImageGallery содержала Value Type c Reference Type хранением, а именно массив  images:[ImageModel] :

Автор статьи «Stop Using Structs!». («Прекратите использовать структуры structs!») предлагает нам сделать предпочтительный выбор класса class для ImageGallery и мы это сделаем:

Теперь мы сделаем Моделью нашей коллекции изображений Collection View не просто саму коллекцию var imageCollection = [ImageModel](),  а всю Галерею изображений var imageGallery = ImageGallery (name: «tt») :

…, которую будем передавать из таблицы Table View по ссылки в методе prepare (for segue: UIStoryboardSegue, sender: Any?) класса GalleriesTableViewController, так как ImageGallery — это class:

Скорректируем class ImageGalleryCollectionViewController под новую Модель :

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

Давайте повторим теперь уже с новой Моделью Галереи Изображений в виде класса class ImageGallery все те эксперименты с нашим приложением, которые мы проделывали до сих пор.

Запускаем приложение. Опять получаем подсвеченную (hightlighted) первую строку таблицы, она не выбрана, нам нужно кликнуть на ней, чтобы сработал наш Segue:

Кликнув на первой строке и видим, что на нашей коллекции появился заголовок «Gallery 1» и изображения, находящиеся в этой Галерее. Давайте добавим несколько изображений из Google в эту Галерею и переименуем ее в «sunrise«:

Вернемся обратно в таблицу и выберем Галерею «Gallery 2«, естественно, она пустая:

Вернемся обратно в Галерею «sunrise«,  мы увидим, что все наши добавленные изображения сохранились:

Мы можем также добавить изображения в «Gallery 2» и переименовать ее в «ocean«:

Мы видим, что технология, когда Модель Галереи Изображений ImageGallery является не структурой struct, а классом class, также работает, но код при этом значительно логичнее и, как утверждает автор статьи «Stop Using Structs!». («Прекратите использовать структуры structs!«), является более эффективным в плане производительности.  Таким образом, вопрос, связанный с выбором  Модель Галереи Изображений ImageGallery решен в пользу класса class.

Теперь кликнем на ячейке, находящейся в секции Recently Deleted. Мы видим, что на MVC коллекции изображений Collection View ничего не меняется: ни заголовок, ни набор изображений, этот MVC показывает «старую» Галерею Изображений «sunrise«, это говорит о том, что «переезд» из Recently Deleted строк не происходит :

В принципе, мы выполнили требование Задания 5: не позволяйте пользователю открывать Галерею изображений из секции Recently Deleted без выполнения сначала операции НЕ УДАЛЕНИЯ (undelete). Но нам бы хотелось, чтобы изображения для Галерей, попавших в эту секцию, не только не отображались, но чтобы добавление изображений было вообще невозможно, и кроме того, нужно более отчетливо информировать пользователя о том, что он работает с Галереей, находящейся в секции Recently Deleted даже если таблица Table View на экране отсутствует. Для этого мы создадим еще один Segue с идентификатором «Not Show Gallery» от ПРОТОТИПА Title для ячеек секции Recently Deleted до MVC коллекции изображений Collection View:

И внесем соответствующие изменения в метод prepare(for segue: UIStoryboardSegue, sender: Any?):

Для ячеек секции Recently Deleted мы используем Segue с идентификатором «Not Show Gallery«. В этом случае мы показываем пустую коллекциию изображений, а именем коллекции newName , которая попадет в заголовок MVC коллекции изображений Collection View мы сообщаем, что эта Галерея Изображений находится в секции Recently Deleted и ее изображения не отображаются. Кроме того, с помощью свойства isUserInteractionEnabled коллекцииivc.collectionView?мы запрещаем взаимодействие пользователя с отображаемой пустой коллекцией и делаем фон коллекции серым:

Если мы попытаемся перетянуть Drag на эту коллекцию изображения, то зеленый знак «+» не появляется и изображение не может быть сброшено Drop:

И оно остается пустым:

Для того, чтобы восстановить возможность добавления изображений для Галерей, расположенных в основной секции, после того, как вы побывали в секции Recently Deleted, мы должны в методе prepare(for segue: UIStoryboardSegue, sender: Any?) для секции 0 присвоить свойству  isUserInteractionEnabled коллекции ivc.collectionView?значение true.

Теперь, если мы после старта приложения кликнем на Галерее Изображений «Gallery 2» (она пока не содержит никаких изображений)…

… то сможем добавлять в эту Галерею какие угодно изображения:

и даже переименовать Галерею «Gallery 2» в «sunset«:

После этого мы можем удалить эту Галерею «sunset» с помощью жеста Swipe влево:

После этого  Галерею «sunset» окажется в секции Recently Deleted, и если мы кликнем на ней в этой секции, то не увидим никаких изображений…

и не сможем ничего добавить в эту Галерею «sunset» (нет зеленого «+»)…

до тех пока не вернем эту Галерею «sunset» в основную секцию с помощью жеста Swipe вправо:

В основной секции 0 свойства редактирования коллекции изображений восстанавливается:

и мы можем продолжить добавлять в Галерею  «sunset» новые изображения:

Еще одна тонкость. Если я попытаюсь вызвать на экран таблицу Table View, то курсор (подсветка) установится на первой строке «Gallery 1«, неправильно показывая нам имя Галереи Изображений:

… потому, что в методе viewDidAppear мы принудительно устанавливаем курсор (подсветка) на первую строку. Давайте это изменим и будем запоминать в переменной lastIndexPath последнюю выбранную пользователем строку:

Запоминание переменной  lastIndexPath будем выполнять в методе prepare(for segue: UIStoryboardSegue, sender: Any?):

Использовать переменную lastIndexPath мы будем в viewDidAppear:

Теперь, при возвращении назад к таблице Table View по стрелке …

мы будем получать подсветку правильной строки:

Код для решения обязательных пунктов Задания 5, когда используются два Segues для различных ПРОТОТИПОВ ячеек, находится на Github для iOS 11 и на Github для iOS 12 в папке ImageGalleryRequiedTwoSegues.

Надо сказать, что наличие двух Segue выглядит не эстетично на storyboard.

Мы можем сделать то же самое, выполняя “переезд” (Segue) прямо в этом коде вручную. Для этого создаем с помощью CTRL-перетягивания Segue между этими двумя View Controllers, а не между ячейками таблицы Table View и Navigation Controller в части Detail.

Когда я кликаю на новом Segue, то он выбирает не ячейки таблицы, а целый View Controller.

Это своего рода обобщенный (generic) Segue между этими двумя View Controllers. Это не приводит к автоматическому «переезду» (Segue). Когда вы создаете Segue между двумя View Controllers, то вам нужно явно в коде в нужном месте просто вызывать метод performSegue с именем нужного Segue:

Этим местом для нас является метод didSelectRowAt делегата таблицы Table View. Метод didSelectRowAt вызывается каждый раз, когда пользователь кликает на любой строке таблицы. В нашем случае мы будем вызывать в методе didSelectRowAt метод performSegue с именем Segue «Show Gallery» и indexPath той строки, на которой кликнул пользователь, в качестве sender. Как и любой другой Segue, наш обобщенный (generic) Segue между этими двумя View Controllers использует метод prepare (for segue: UIStoryboardSegue, sender: Any?): для подготовки того MVC, на который он переезжает, но в нашем случае он сильно упростится:

Кроме того, мы наконец-то сможем выполнить ВТОРУЮ ЧАСТЬ дополнительного пункта 3 :

Пункт 3 дополнительный

Все время держите таблицу Table View в синхронном состоянии с вашей коллекцией Collection View. Это может быть немного более хитроумно, чем кажется. По существу, вам следует ВСЕГДА иметь выбор строки (selection) в таблице Table View, и эта выбранная строка должна ВСЕГДА соответствовать тому, что показывается в коллекции Collection View. Таблица Table View может быть весьма привередливой относительно выбора строк с помощью selectRow (at:). Если она заканчивает анимацию, например, то выбор строки может не состояться. Возможно, вам следует использовать таймер Timer для того, чтобы задержать немного выбор строки. Конечно, вы также должны использовать performSegue, когда выбираете строку от имени пользователя. Есть также ошибка выбора строки при первом появлении таблицы Table View. Возможно, вам следует ждать по крайней мере до viewDidAppear:, чтобы сделать это.

Как уже говорилось выше, этот пункт состоит из двух частей: сначала мы должны обеспечить выбор в таблице Table View определенной строки indexPath.row в определенной секции indexPath.section, а потом обеспечить показ коллекции изображений, соответствующей этой ячейки, в MVC с коллекцией изображений Collection View.

Мы уже реализовали ПЕРВУЮ ЧАСТЬ — выбор нужной строки таблицы Table View с помощью нашего private метода selectRow (at:, after: )  с соответствующей задержкой after, если это необходимо. Теперь мы с помощью одной строчки кода сможем выполнить ВТОРУЮ ЧАСТЬ — синхронизацию ячейки таблицы Table View с коллекцией Collection View, отображающей коллекцию изображений:

Этой единственной добавленной строчкой будет выбор, именно выбор, а не подсветка, строки таблицы Table View с помощью метода таблицы didSelectRowAt. Вызов этого метода аналогичен тому, что пользователь кликает на этой строке таблицы.

Теперь, при старте приложения и тогда, когда мы удаляем Галерею Изображений, которая в данный момент показывается в коллекции Collection View, и тогда, когда мы изменяем имя Галереи Изображений name, и тогда, когда возвращаем Галерею Изображений назад в основную секцию с помощью “жеста swipe в другом направлении”,  нам не нужно будет кликать на подсвеченной строке, все уже будет готово для пользователя, он будет видеть соответствующую коллекцию изображений Collection View и ее заголовок.

Стартуем приложение:

Вы видите, что сразу же отображается заголовок и коллекция изображений для Галереи «Gallery 1«.

Давайте добавим новые изображения в эту Галерею:

и переименует ее в «sunset«:

Новый заголовок сразу же отображается, как только вы нажали клавишу Return.

Удалим эту Галерею «sunset«:

Как только мы кликнем на кнопке Delete, Галерея «sunset» переместится в секцию Recently Deleted и сразу же отобразится там соответствующим образом, то есть без коллекции изображений и с невозможностью добавлять новые:

Вернем Галерею «sunset» обратно в основную секцию:

Как только мы кликнем на кнопке Undelete, Галерея «sunset» переместится в  основную секцию Recently Deleted и сразу же отобразится там соответствующим образом, то есть с заголовком и коллекцией изображений и получит возможность добавлять новые изображения:

За все время наших экспериментов мы ни разу не кликнули на строке, все происходит автоматически. С таким приложением работать очень комфортно.

Но есть один нюанс. Когда мы уже выбрали Галерею изображений и, работая с ее коллекцией изображений, убираем и заставляем вновь появляться таблицу Table View, не выбирая из нее новой Галереи, то нам не нужно каждый раз при этом выбирать и перезагружать коллекцию изображений с помощью метода didSelectRowAt. В этом случае мы работаем с переменной lastIndex в методе viewDidAppear и не будем использовать private метод selectRow(at:),  а вернемся к «родному» методу таблицы tavleView:

Таким образом, мы выполнили все обязательные пункты Задания 5 и даже один дополнительный. Этот вариант кода находится на Github в папке ImageGalleryRequiedGenericSegue.

Решение обязательных пунктов без Segue

На самом деле, если мы используем наше приложение на iPad (именно это оговорено в обязательном пункте 17 Задания 5), то в Split View Controller наш Detail, которым является MVC с коллекцией изображений Collection View, всегда находится на экране. Поэтому нам вообще нет смысла использовать «переезд» (Segue) на него.  Мне достаточно посмотреть на свой splitViewController, найти в нем этот ViewController, соответствующий MVC c моей коллекцией  ImageGalleryCollectionViewController, и начать разговаривать с ним напрямую. 

Пользуясь переменной  splitViewDetailCollectionController, позволяющей получить Detail, создадим метод showCollection (at: IndexPath), в который перенесем из метода prepare (for segue: UIStoryboardSegue, sender: Any?) все настройки полученного Detail:

Теперь в методе таблицы didSelectRowAt мы будем использовать НЕ метод performSegue, а метод showCollection (at: IndexPath):

В отличие от «переезда» с помощью Segue, когда каждый раз создается новый экземпляр Detail, в метод showCollection (at: IndexPath) ВСЕГДА будет использоваться один и тот же уже существующий экземпляп Detail, что требует некоторых минимальных дополнений в коде самого Detail :

Если ссылочный адрес Галереи imageGallery не изменился, то мы не будем выполнять такую затратную операцию, как загрузка всех изображений заново. Это относится к оператору ===, применяемому к Rererence Types. Это обеспечивает более «гладкое» функционирование приложения, например, в случае, если мы хотим редактировать имя Галереи Изображений и используем для этого жест «двойной» tap. В случае с Segue иногда «двойной» tap распознается как два «одинарных» tap, что приводит к срабатыванию Segue, повторной загрузки нового экземпляра Detail и всех его изображений. При отсутствии Segue повторной загрузки изображений не происходит, благодаря приведенному выше коду. Если повторной загрузки нового экземпляра Detail и всех его изображений не происходит при работе с неизменной Галереей Изображений, то мы можем вернуться к более элегантному варианту метода viewDidAppear нашего GalleriesTableViewController:

Мы удаляем Segue c идентификатором «Show Gallery» и его метод подготовки prepare (for segue: UIStoryboardSegue, sender: Any?) :

Мы можем провести все прежние эксперименты с приложением, чтобы убедиться, что оно работает правильно.

Стартуем приложение:

Вы видите, что сразу же отображается заголовок и коллекция изображений для Галереи «Gallery 1«.

Давайте добавим новые изображения в эту Галерею:

и переименует ее в «sunset«:

Новый заголовок сразу же отображается, как только вы нажали клавишу Return.

Удалим эту Галерею «sunset«:

Как только мы кликнем на кнопке Delete, Галерея «sunset» переместится в секцию Recently Deleted и сразу же отобразится там соответствующим образом, то есть без коллекции изображений и с невозможностью добавлять новые:

Вернем Галерею «sunset» обратно в основную секцию:

Как только мы кликнем на кнопке Undelete, Галерея «sunset» переместится в  основную секцию Recently Deleted и сразу же отобразится там соответствующим образом, то есть с заголовком и коллекцией изображений и получит возможность добавлять новые изображения:

За все время наших экспериментов мы ни разу не кликнули на строке, все происходит автоматически, но очень плавно и  без лишних перезагрузок коллекции изображений Collection View. С таким приложением работать очень комфортно.

Итак, наше приложение готово, и мы должны убрать из него тестовые данные для imageGalleries:

В результате мы будем начинать работать с нашим приложением, когда массив Галерей Изображений imageGalleries пуст:

И тут нас ждет сюрприз:

Мы получили аварийное завершение приложения из-за выхода индекса массива за допустимые пределы. В пустом двумерном массиве [[ImageGallery]] () не существует 0-ой секции section. Для правильной работы методов UITableViewDataSource следует создать хотя бы одну секцию с одной записью в ней:

Это довольно удобно для пользователя. Мы начинаем с готовой Галереи Изображений с фиксированным слишком обобщенным и ничего не говорящем о специфики Галереи названием «Gallery 1«:

И сразу можем начать формировать коллекцию изображений для этой Галереи:

Затем мы можем вернуться к таблице с именами Галерей и переименовать нашу Галерею в «autumn«:

На этом сюрпризы не заканчиваются. Если мы попробуем удалить Галерею «autumn«…

… то получим ошибку, которая говорит о том, что у нас нет секции 1 и в Модели imageGalleries отсутствует массив imageGalleries [1]:

В этом случае мы должны вставить в imageGalleries целый массив, содержащий удаленную из секции 0 строку и создать новую секцию:

В этом случае мы не можем использовать метод performBatchUpdates, так как образовалась новая секция и нам требуется более «тяжелый» случай обновления таблицы tableView.reloadData().

Если теперь мы попытаемся удалить  Галерею «autumn«…

… то она просто переместится в секцию Recently Deleted:

В любой момент мы сможем вернуть  Галерею «autumn» обратно, в основную секцию с помощью кнопки Undelete:

И мы получаем Галерею «autumn» в основной секции:

Этот вариант кода находится на Github для iOS 11 и на Github для iOS 12 в папке ImageGalleryRequiedNoSegue.

На этом выполнение обязательных пунктов закончено. Выполнение дополнительных пунктов Задания 5, из которых осталось только 2 — первый и второй, будет описано в следующем посте.
Продолжение следует…