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

Содержание

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

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

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

Этот пост мы посвятим оставшимся дополнительным пунктам (Extra Credit) Задания 5.

Мое решение обязательных и дополнительных пунктов Задания 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 (обязательные пункты в наиболее комфортном для пользователя исполнении)
  6. ImageGalleryRequiedNoSegueExtra — ОКОНЧАТЕЛЬНЫЙ ВАРИАНТ Задания 5 с обязательными и дополнительными пунктами.

Пункт 1 дополнительный (Extra Credit)

Позвольте пользователям перетаскивать Drag элементы коллекции Collection View в “мусорный бак” (возможно на навигационной панели вверху), что удалит этот URL из Галереи изображений (Image Gallery).

Идея того, как это можно сделать, заключается в следующем.

На навигационной панели мы размещаем некоторое  UIView с «мусорным баком»…

…и реализуем для него Drop сессию «сброса» изображения из нашей коллекции.

В процессе «сброса» Drop происходит анимация сбрасываемого изображения с уменьшением масштаба   …

… практически до нуля:

… и в конце концов изображение исчезает в «мусорном баке» и исчезает из коллекции:

Создание UIView c «мусорным баком».

Создаем новый класс GarbageView, который наследует от UIView и содержит «мусорный бак» в качестве subview:

На этом рисунке специально изменены цвета фона для самого GarbageView и кнопки UIButton с изображением «мусорного бака» (на самом там прозрачный фон) для того, чтобы вы видели, что у пользователя, который сбрасывает изображения Галереи в «муссорный бак», гораздо больше пространства для маневра при сбросе Drop.

У класса GarbageView два инициализатора  и оба используют метод setup():

Кроме того, класса GarbageView реализует протокол «сброса» UIDropInteractionDelegate:

В методе setup() я добавляю к GarbageView “взаимодействие” dropInteraction и это будет UIDropInteraction, так как я хочу научить GarbageView  получать и обрабатывать «сброс» Drop изображения из коллекции. Все, чем мы должны обеспечить этот UIDropInteraction, это делегат delegate, и я назначаю себя, self, этим делегатом delegate:

В  методе setup() я также добавляю в качестве  subview кнопку myButton с изображением «мусорного бака», взятым из стандартной Bar Button кнопки Trash:

И устанавливаю прозрачный фон для GarbageView:

Размер «мусорного бака» и его место положение будет определяться в методе layoutSubviews() класса UIView в зависимости от границ bounds:

Все, что нам нужно сделать, чтобы заставить работать Drop, это реализовать уже известные нам методы canHandle, sessionDidUpdate и performDrop делегата UIDropInteractionDelegate.

Давайте сделаем это.

Внутри метода canHandle мы говорим, что будем обрабатывать только те перетаскивания Drag, которые представляют собой изображения UIImage. Поэтому я верну true только, если session.canLoadObjects(ofClass: UIImage.self):

В методе canHandle по существу вы просто сообщаете, что если перетаскивание Drag не является перетаскиванием этого типа, то даже не говорите со мной о нем. Если же оно является перетаскиванием того типа, то мы будем с ним разговаривать, и я выполняю метод sessionDidUpdate. Все, что нам нужно сделать в этом методе, это вернуть наше предложение UIDropProposal по сбросу Drop . И я готова принять Drag, содержащий изображение UIImage, которое будет сброшено Drop где угодно внутри моего GarbageView. Поэтому я возвращаю предложение сброса в виде конструктора UIDropProposal с аргументом operation, принимающим значение .copy, если «перетаскивание» Drag в моем приложении будет происходить из коллекции Collection View, то есть локально. «Перетаскивание» изображения UIImage ИЗВНЕ запрещено, поэтому аргумент operation принимает значение .forbidden

Копируя изображение UIImage, мы будем имитировать уменьшение его масштаба практически до 0, а когда «сброс» произойдет, мы удалим это изображение из коллекции Collection View.

Теперь, если пользователь поднял палец вверх, то происходит «сброс» Drop, и я (как GarbageView) получаю сообщение performDrop. В сообщении performDrop мы выполняем собственно «сброс» Drop. Честно говоря, само сброшенное на GarbageView изображение нас больше не интересует, так как мы сделаем его практически невидимым, скорее всего сам факт завершения «сброса» Drop послужит сигналом к тому, чтобы мы убрали это изображение из коллекции Collection View. Для того, чтобы это выполнить, мы должны знать саму коллекциию collection и indexPath сбрасываемого изображения в ней. Откуда мы их можем получить?

Поскольку процесс Drag & Drop  происходит в одном приложении, то нам доступно всё локальное: локальная Drag сессия localDragSession нашей Drop сессии session, локальный контекст localContext, которым является наш UICollectionView и локальный объект localObject, которым мы можем сделать само сбрасываемое изображение из «Галереи» или его indexPath. Благодаря этому мы можем получить в методе performDrop класса  GarbageView  коллекцию collection, а используя ее dataSource как ImageGalleryCollectionViewController и Модель imageGallery нашего Controller, мы можем получить массив изображений images ТИПА [ImageModel]:

С помощью локальной Drag сессии localDragSession нашей Drop сессии session нам удалось получить все перетягиваемые на GarbageView Drag элементы items, а их может быть много, как мы знаем, и все они являются изображениями нашей колллекции. Создавая Drag элементы dragItems нашей коллекции Collection View, мы предусмотрели для каждого перетягиваемого Drag элемента dragItem  локальный объект localObject, который является индексом indexPath в массиве изображений images нашей Модели imageGallery :

Если вы выполняете «перетаскивание» Drag локально, то есть внутри вашего приложения, то вам нет необходимости проходить через весь код, связанный с itemProvider, через асинхронное получение данных, когда мы реализуем Drop. Вам не нужно ничего этого делать, вам нужно просто взять localObject и использовать его. Это своего рода “короткое замыкание” при локальном перетаскивании Drag.

Именно так мы и поступим. Будем брать у каждого претаскиваемого элемента item его localObject, который является индексом indexPath в массиве изображений images нашей Модели imageGallery, и отправлять его в массив индексов indexes и массив indexPahes удаляемых изображений :

Зная массив индексов indexes и массив indexPahes удаляемых изображений, в методе performBatchUpdates коллекции collection мы убираем все удаляемые изображения из Модели images и из коллекции collection:

В заключении мы сообщаем, что наш GarbageView изменился, но об этом позже. Для того, чтобы создать у пользователя иллюзию «сброса и исчезновения» изображений в «мусорном баке», мы используем метод previewForDropping, который позволяет перенаправить «сброс» Drop в другое место и при этом трансформировать сбрасываемый объект в процессе анимации:

В этом методе c помощью инициализатора UIDragPreviewTarget мы получим новый preView для сбрасываемого объекта target и перенаправим его с помощью метода retargetedPreview на новое место, на «мусорный бак», с уменьшением его масштаба практически до нуля:

GarbageView может занимать гораздо больше места, чем иконка «мусорного бака», давая пользователю больше возможностей для маневра:

Но где бы в GarbageView мы не сбросили изображение, оно будет перенаправлено в «мусорный бак» и его масштаб уменьшится до 0.1:

После анимации изображение, сбрасываемое в «мусорный бак»,  исчезнет из коллекции :

Наш код рассчитан на то, что пользователь может сбрасывать более одного изображения в «мусорный бак»:

Мы разместим кнопку «мусорный бак» garbageView на навигационной панеле в нашем ImageGalleryCollectionViewController в методе viewDidLayoutSubviews() :

Код находится на Github  для iOS 11 и на Github для iOS 12 в папке ImageGalleryRequiedNoSegueExtra.

Пункт 2 дополнительный ( extra credit)

Сделайте так, чтобы Галереи изображений (Image Gallery) постоянно сохранялась между запусками вашего приложения с помощью UserDefaults. Мы собираемся изучать “постоянное хранение” (persistence) на следующей неделе, но, возможно, мы не будем использовать UserDefaults, так что у вас есть хорошая возможность изучить это сейчас.

Мы добавим в GalleriesTableViewController вычисляемую переменную imageGalleriesJSON, которая имеет тот же самый ТИП [[ImageGallery]]?, как и наша Модель imageGalleries, но только Optional:

В set {} мы кодируем с помощью JSONEncoder объект newValue ТИПА [[ImageGallery]] в JSON, который имеет ТИП Data и прекрасно сохраняется в UserDefault. Мы сохраняем его в UserDefault с ключом «SavedGalleries«. Все эти действия осуществляются, если объект newValue не равен nil.

В get {} мы пытаемся достать сохраненную и закодированную Модель ТИПА [[ImageGallery]] из хранилища  UserDefault, в котором она хранится как объект ТИПА Data с ключом «SavedGalleries«. Сначала мы достаем из UserDefault объект с  ключом «SavedGalleries» и убеждаемся, что он имеет ТИП Data,  затем декодируем ее с помощью JSONDecoder в loadedGalleries, который имеет ТИП [[ImageGallery]]. Если в UserDefault нет объекта с ключом  «SavedGalleries» или не удалось декодировать полученный объект в [[ImageGallery]], то возвращаем nil.

Кодировка и декодировка нашей Модели var imageGalleries = [[ImageGallery]] () возможна при условии, что class ImageGallery реализует протокол Codable. Это происходит автоматически, если все его переменные vars также реализуют протокол Codable:

Строки String, массивы Array, URL и Double уже реализуют протокол Codable, поэтому нам больше ничего не придется делать, чтобы заставить работать кодировку и декодировку для модели var imageGalleries = [[ImageGallery]] ().

Теперь мы должны где-то присвоить нашей Модели imageGalleries значение переменной imageGalleriesJSON, и тогда сработает get{} и мы получим значение, сохраненное в UserDefaults. Мы будем это делать в методе viewDidLoad нашего GalleriesTableViewController:

Если нам не удастся получить Галереи Изображений из UserDefaults, то мы создаем единственную Галерею Изображений с именем «Gallery 1«.

В методе viewWillDisapper нашего GalleriesTableViewController мы выполним обратную операцию — присвом imageGalleriesJSON значение нашей Модели imageGalleries и тем самым заставим сработать set {}, который закодирует и запишет нашу измененную Модель в UserDefaults:

Теперь мы можем наполнить наше приложение множеством Галерей Изображений:

…и в одну из них, например, Галерею «sunset» …

… мы добавим еще одно изображение:

После того, как новое изображение разместилось в коллекции…

… мы можем нажать кнопку «Home» или выполнить жест ее заменяющий, и приложение уйдет с экрана, запомнив в UserDefault все Галереи Изображений.

Мы даже можем удалить приложение из списка приложений, запущенных в данный момент на iPad:

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

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

Код находится на Github   для iOS 11 и на Github для iOS 12 в папке ImageGalleryRequiedNoSegueExtra.

Задание 5 закончено.