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

Содержание

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

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

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

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

Мое решение Задания 5 находится на Github для iOS 11 и на Github для iOS 12 в папке ImageGallery_V. Для этого поста это вариант ImageGalleryOnly.

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

Логически выполнение обязательных пунктов Задания 5 распадается на две части: первая часть ( ей посвящен этот пост) будет обеспечивать работу одной Галереи Изображений Image Gallery и будет использовать исключительно коллекцию Collection View, а вторая часть будет обеспечивать работу со списком имен Галерей Изображений с помощью таблицы Table View, функционирование которой мы отработаем отдельно, а затем подстыкуем к ней коллекцию Collection View, уже настроенную под Галерею изображений Image Gallery.

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

Пункты 1, 17 обязательные

1. Создайте приложение, центральной частью которого является коллекция Collection View, который содержит изображения, “перетянутые” на него с помощью механизма Drag & Drop. Коллекция такого рода изображений называется “Image Gallery” (“Галерея изображений”).

17. Это исключительно iPad приложение (по крайней мере на этой неделе).

Для того, чтобы понять концепцию и цели данного приложения давайте попробуем представить, как будет выглядеть приложение, отображающее коллекцию изображений и позволяющее наполнять ее с помощью механизма Drag & Drop из других приложений. Конечно, это прежде всего должно быть обычное приложение, отображающее коллекцию изображений, наподобие следующего приложения, работающего с тестовые изображения:

  1. Эйфелева башня
  2. Венеция
  3. Шотландский замок
  4. Арктика

Затем в эту коллекцию встраивается механизм Drag и Drop, с которым мы будем работать в многозадачном режиме Slide Over или Splite View, позволяющий одновременно разместить на экране два приложения: наше приложение Image Gallery и любое другое приложение, содержащее изображения, например, Safari с поисковой системой Google. Для перехода в многозадачный режим Slide Over необходимо выполнить следующие действия:

  1. Сначала мы запускаем наше приложение Image Gallery, которое полностью оккупирует экран.
  2. Затем cмахиваем вверх от нижнего края экрана с помощью очень короткого жеста  swipe up, чтобы открыть панель Dock.
  3. На панели Dock нажимаем и удерживаем в качестве второго приложения Safari, и перетаскиваем его вправо, расположив его на экране бок-о-бок  с нашим приложением Image Gallery

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

Здесь мы ищем изображения на тему «Рассвет» (sunrise). В Safari уже встроен Drag & Drop механизм, поэтому мы можем выделить одно из этих изображений, долго удерживать его и немного сдвинуть. 

Видите? Что происходит? Выбранное мною изображение “поднимается”, и я могу перетащить его в мою Галерею Изображений. Если допустить, что в моем приложении Image Gallery уже встроен механизм Drag & Drop для изображений, «сбрасываемое» изображение будет отображаться в моем приложении с маленькой зеленым плюсиком «+» в правом верхнем углу…

… и после «сброса» разместится в моей Галереи Изображений:

Мы можем «набросать» множество таких изображений на тему «Рассвет»и перемещать их на любое новое место в коллекции…

Мы можем выбросить ненужное нам изображение в «мусорное ведро» путем перетаскивания изображения на навигационную панель к иконке «Trash«:

В результате это изображение будет удалено из коллекции:

Вот какое приложение в общих чертах мы должны создать в этом посте. Будем создавать это приложение «с нуля». Создаем совершенно новый проект в Xcode.

Как всегда используем шаблон Single View app:

Мы назовем наш проект  ImageGallery.

Мы, конечно, разместим наше новое приложение там же, где находятся другие проекты. Также, как рекомендует профессор, я собираюсь немного почистить мой проект и убрать некоторые файлы…


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

Я собираюсь удалить этот обобщенный (generic) ViewController, который я получила вместе с шаблоном приложения Single View app, и я просто размещу его в “корзине с мусором”:


Это моя storyboard и, конечно, мне нужен свой собственный Controller и свой собственный View для этого ImageGallery . Я удаляю экранный франгмент, который я получила вместе с шаблоном приложения Single View app, предварительно выделив его с помощью желтой иконки в самой верхней его части: 

В  результате storyboard пуста…

…, и мы в буквальной смысле слова начинаем с «чистого листа».

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

Этому Controller, конечно, нужен свой собственный subclass класса UICollectionViewController.Я иду в меню File -> New -> File …

… и создаю пользовательский UICollectionViewController, который я назову ImageGalleryCollectionViewController:

Это будет главный View Controller этого приложения и мы разместим в нем все, что требуется в Задании 5:

Я иду на storyboard и мне нужно убедиться, что я установил этот пользовательский класс ImageGalleryCollectionViewController этому View Controller в Инспекторе Идентичности:

Я буду создавать мой UI на iPad, так как это приложение будет работать только на iPad в силу того, что механизм Drag & Drop между приложениями работает только на iPad. Давайте установим ландшафтный режим для iPad Pro 9,7».

Что мы имеем? В отличие от демонстрационного примера Лекции 12, у нас уже имеется готовая встроенная в Image Gallery Collection View Controller коллекция Collection View с ячейкой Collection View Cell:

Кроме того, если CTRL-кликнем на этой коллекции Collection View, то увидим, что наш Image Gallery Collection View Controller уже является «источником данных» dataSource и делегатом delegate для коллекции Collection View

и нам не нужно подтверждать протоколы UICollectionViewDataSource и UICollectionViewDelegate:

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

…, все обязательные методы «источника данных» dataSource уже размещены в нашем Controller шаблоном, который вы получите при создании subclass класса UICollectionViewController:

Все методы протокола UICollectionViewDelegate — необязательные, часть из них представлена в закомментированном состоянии, но они нам не нужны, поэтому избавляемся от них:

Кроме того, шаблон, который я получила при создании subclass класса UICollectionViewController, вызываетregisterClass в viewDidLoad:

УДАЛЯЕМ ЭТУ СТРОКУ КОДА и все, что с ней связано. Вместо этого мы будем устанавливать идентификатор повторно используемой ячейки коллекции и задавать ее класс как subclass класса UICollectionViewCell на storyboard. Если НЕ уничтожить этот вызов registerClass, то он переопределит все, что вы задали на storyboard, так как viewDidLoad вызывается после того, как выполнена загрузка со storyboard.

В результате наших действий мы естественно получим ошибку в методе cellForItemAt indexPath:

Мы заменим reuseIdentifier на идентификатор «Image Cell», который установим через секунду на storyboard:

Пока мы находимся в коде я попутно удалю ненужный нам метод «жизненного цикла» View Controller:

Оставляем закомментированный метод prepare (for segue:), необходимый нам в дальнейшем для навигации, и получаем вот такой компактный ImageGalleryCollectionViewController:

Однако эта коллекция ничего не показывает, так как прототип ячейки Collection View Cell коллекции Collection View — пуст, но давайте для начала присвоим ей обещенный идентификатор «Image Cell» для «повторно используемых» ячеек коллекции, это тот идентификатор, который я использую в методе collectionView.dequeueReusableCell :

Мы должны чем-то наполнить прототип ячейки Image Cell, и мы плавно переходим к обязательному пункту 2.

Пункты 2, 7 и 8 обязательные

2. Разрешайте “сбрасывать” Drop только те элементы, у которых есть и UIImage изображения, и URL изображения.

7. Каждый раз, когда изображение выбирается в ячейку коллекции из URL, индикатор активности (activity indicator) должен вращаться в этой ячейке, давая знать пользователю, что вы над этим работаете.

8. Не кэшируйте изображения. Выбирайте их каждый раз из их URL, если они вам нужны.

Мы должны разместить в ячейке Image Cell коллекции Collection View изображение UIImage, но предварительно давайте увеличим ее размер, чтобы проще было обращаться с UI этой ячейки, хотя реальный размер ячейки на storyboard не имеет принципиального значения из-за того, что мы все равно будем устанавливать его в коде. Тем не менее установим размер ячейки коллекции равным 300 х 300:

Вытягиваем из Палитры Объектов Image View и размещаем его на прототипе ячейки Image Cell:

Для моего нового Image View я также использую мой любимый инструмент для создания ограничений (constraints) для того, чтобы «пришпилить» его краям ячейки Image Cell.

Еще перетягиваем из Палитры Объектов индикатор активности Active indicator View в нашу ячейку Image Cell:

Мы изменим стиль индикатора активности Active indicator View на Large White и изменим его цвет на голубой, кроме того, мы расположим его посередине ячейки Image Cell на переднем плане:

Теперь мы сможем подгрузить изображение UIImage в эту ячейку Image Cel. Для этого нам нужны Outlets к нашим UI элементам: изображение Image View и индикатору активности Active indicator View, а, следовательно, у вас должен быть subclass классаUICollectionViewCell, так как для коллекции CollectionView все ячейки cells являются пользовательскими (Custom) ячейками cells. Вы не можете иметь в нашем собственном классе ImageGalleryCollectionViewController эти Outlets, которые указывают на изображение и индикатор, так как в коллекции могут быть сотни ячеек cells. Вы не можете так делать. Вместо этого вы должны создать с помощью меню File -> New -> File новый файл…

….который будет классом Сocoa Touch Class …

… и subclass класса UICollectionViewCell. Я назову его ImageCollectionViewCell:


Мы размещаем его в правильном месте.

Вот этот новый subclass класса UICollectionViewCell:

Я иду на мою storyboard, инспектирую ячейку Image Cell и устанавливаю идентичность ячейки в Инспекторе Идентичности, указывая класс  ImageCollectionViewCell:

Теперь моя ячейка Image Cell имеет ТИП моего subclass, то есть
ImageCollectionViewCell
, и идентификатор повторно используемой ячейки, Image Cell:


Теперь, когда у меня все это есть, идем вперед и “подвязываем” наше изображение Image View и индикатор активности Active indicator View к нашему  subclass ImageCollectionViewCell. Размещаем еще и код одновременно со storyboard на экране, нажимая кнопку Ассистента Редактора:

Я создаю Outlet в моем пользовательском subclass ImageCollectionViewCell с помощью CTRL-перетягивания к изображению Image View  и называю его imageGallery:

Я создаю Outlet с помощью CTRL-перетягивания к индикатору активности Active indicator View  и называю его spinner:

В результате мы получаем два Outlets в нашем пользовательском subclass ImageCollectionViewCell :

Но subclass ImageCollectionViewCell класса UICollectionViewCell могут также иметь логику. Он не ограничиваются только тем, что являются просто контейнером для Outlets. Сделаем Моделью класса ImageCollectionViewCell URL изображения — Optional значение imageURL?, а задачей этого класса — выборку данных изображения по этому URL и размещение его в нашем imageGallery:

Если значение imageURL не равно nil, то мы пытаемся выбрать данные изображения с помощью строки:

let urlContents = try? Data(contentsOf: url)

Попутно запускаем индикатор активности spinner:

spinner?.startAnimating()

Если нам удалось выбрать эти данные imageData и превратить их в изображение image с помощью UIImage(data: imageData), то вы размещаете их в вашем UI

imageGallery?.image =  image

…  и останавливаем индикатор активности spinner:

spinner?.stopAnimating()

Код досаточно простой, но проблема в том, что я НЕ МОГУ ВЫПОЛНИТЬ строку кода

let urlContents = try? Data(contentsOf: url)

на main queue, потому что она заблокирует весь мой UI. Мы должны убрать эту строку кода прочь с main queue на другой поток. Остальной код будет прекрасно работать на main queue. Как работать с многопоточностью профессор рассказывает на Лекции 10.

Пункты 6 и 8 обязательные

6. Никогда не блокируйте main thread. Выборка изображений по URLs должна осуществляться за пределами main queue.

8. Не кэшируйте изображения. Выбирайте их каждый раз из их URL, если они вам нужны.

Я использую для получения данных по заданному URL глобальную очередь global (qos: .userInitiated) с аргументом «качества обслуживания» qos, который я установлю в .userInitiated, потому что пользователь просит меня что-то выполнить:

Каждый раз, когда вы используете собственные переменные, в наше случае imageGallery, imageURL, внутри замыкания, компилятор заставляет вас ставить перед ними self., чтобы вы спросили себя: «А не возникает ли здесь “циклическая ссылка памяти” (memory cycle)?» У нас нет здесь явной “циклической ссылки памяти” (memory cycle), потому что у самого self нет указателя на это замыкание.

Тем не менее, в случае многопоточности вы должны принять во внимание, что ячейки cells в коллекции являются повторно-используемыми благодаря методу dequeueReusableCell. Каждый раз, когда ячейка (новая или повторноиспользуемая) попадает на экран, запускается асинхронно загрузка изображения из сети (в это время крутится «колесико» индикатора активности spinner), как только загрузка выполнена и изображение получено, происходит обновление UI этой ячейки коллекции. Но мы не ждем загрузки изображения, мы продолжаем прокручивать коллекцию и примеченная нами ячейка коллекции уходит с экрана, так и не обновив свой UI. Однако снизу должно появится новое изображение и эта же ячейка, ушедшая с экрана, будет использована повторно, но уже для другого изображения, которое, возможно, быстро загрузится и обновит UI. В это время вернется ранее запущенная в этой ячейки загрузка изображения и обновит экран, что неправильно. Это происходит потому, что мы запускаем разные вещи, работающие с сетью в разных потоках. Они возвращаются в разное время. Как мы можем исправить ситуацию? В пределах механизма GCD мы не можем отменить загрузку изображения ушедшей с экрана ячейки, но мы можем, когда приходят наши imageData из сети, проверить URL, который вызвал загрузку этих данных, и сравнить его с тем, который пользователь хочет иметь в этой ячейки в данный момент, то есть imageURL. Если они не совпадают, то мы не будем обновлять UI ячейки и подождем нужные нам данные изображения:

Эта абсурдная на первый взгляд строка кода url == self.imageURL заставляет все работать правильно в многопоточной среде, которая требует нестандартного воображения. Дело в том, что некоторые вещи в многопоточном программировании осуществляются в другом порядке, чем написан код.

Если вы хотите побольше узнать о GCD и поэкспериментировать с ним, то можно познакомиться со статьей Многопоточность (concurrency) в Swift 3. GCD и Dispatch Queues на сайте habr.сom.

Итак, мы полностью подготовили ячейку cell коллекции Collection View для загрузки и отображения изображения по заданному URL.

Пришло время подумать, что будет “public (то есть  не private) API” для нашего Controller ImageGalleryCollectionViewController.

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

Используйте UIImage при “сбросе” Drop с одной единственной целью: определить Aspect ratio (соотношение сторон), с которым будет показано изображение, выбранное из URL (то есть не используйте “сброшенное” (Dropped) изображение UIImage для действительного  рисования чего-нибудь).

Очевидно, Моделью для класса ImageGalleryCollectionViewController является массив изображений [ImageModel], где каждое изображение представлено структурой  struct ImageModel, содержащий URL и Aspect ratio (соотношение сторон) изображения UIImage. Для Модели коллекции изображений и последующей Модели Галереи Изображений создадим с помощью меню File -> New -> File новый Swift File

… который я назову ImageGalery. В этом файле мы разместим Mодель изображения ImageModel и Модель Галереи Изображений ImageGalery:

Возвращаемся к нашей коллекции Collection View и реализуем методы dataSource, используя в качестве Модели массив изображений [ImageModel] :

Метод numberOfSections можно убрать, так как по умолчанию и так предполагается  одна секция.

Мы будем добавлять изображения в нашу Галерею Изображений, используя механизм Drag & Drop, но предварительно мы можем проверить работоспособность нашей Colleсtion View с помощью серии тестовых изображений, которые мы оформим в тестовый массив imageCollection в методе viewDidLoad:

Запускаем приложение и … получаем черный экран:

Нам говорят, что НЕ установлена точка входа в наше приложение. Мы должны пойти на storyboard и сделать наш View Controller точкой входа:

Снова запускаем приложение и …получаем ошибку:

Вы узнаете? Это система безопасности App Transport Security

Все наши URLs имеют НЕ безопасный адрес http://, для доступа к которому требуется специальное разрешение системы безопасности, система безопасности разрешает по умолчанию только https://. Мы можем исправить эту проблему, отредактировав наш Info.plist. Для этого воспользуемся подсказкой № 19:

Если вы перетаскиваете Drag URL, который не является безопасным (то есть у него http:// вместо https://), то по умолчанию он не будет принят UIImage. Вы можете изменить эту ситуацию в вашем Info.plist файле. Кликните на Info.plist, затем CTRL-кликните на его фоне и выберите опцию Add Row из контекстного меню, которое “всплывает”, затем прокручивайте список пока не наткнетесь на App Transport Security Settings, кликните на маленьком треугольнике слева на новой строке, это развернет треугольник “носиком” вниз, затем кликаем на кнопке с + чтобы добавить подстроку, выбираем Allow Arbitrary Loads и устанавливаем этот атрибут в YES. После того, как вы все это проделаете, у вас должна быть запись в вашем Info.plist файле наподобие следующей…

Запускаем приложение  и получаем все изображение одинакового размера и одинаковой квадратной формы вне зависимости от их Aspect Ration (соотношения сторон), появление их сопровождалось вращающимся «колесиком» индикатора активности:

Но у нас другое задание на размер изображений в Галереи Изображений.

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

Каждый элемент item в коллекции Collection View должен иметь одну и ту же ширину width, однако высота height каждого элемента item должна определяться с помощью Aspect ratio (соотношение сторон) по отношению к этой ширине width.

В коллекции Collection View вы можете регулировать размер элемента item с помощью метода sizeForItemAt:

Элементами items являются прямоугольники в коллекции Collection View, поэтому мы должны вернуть в этом методе размер CGSize прямоугольника.

Заметьте, что в методе для коллекции Collection View есть дополнительный аргумент помимо sizeForItemAt, это — расположение layout collectionViewLayout. Это “волшебная” вещь, которая принимает по умолчанию значение UICollectionViewFlowLayout, соответствующее “потоковому” расположению элементов коллекции. Но если у вас есть свои собственные расположения collectionViewLayout, то вы можете в этом методе задавать другие типы расположения ячеек layouts. По умолчанию используется так называемое “потоковое” размещение элементов коллекции UICollectionViewFlowLayout, они “перетекают” с одной строки на другую. Кстати, сам метод sizeForItemAt является методом делегата “потокового” расположения элементов коллекции UICollectionViewFlowLayout. Поэтому мы должны подтвердить протокол UICollectionViewDelegateFlowLayout.

В методе вычислим высоту height ячейки коллекции с помощью Aspect Ration (соотношения сторон) изображения imageCollection[indexPath.item].aspectRatio:

Запускаем приложение  и получаем изображения различного размера в зависимости от их Aspect Ration (соотношения сторон):

Так как все ячейки коллекции имеют одинаковую ширину, нам бы хотелось использовать с максимальной пользой пространство по ширине width для размещения фиксированного числа ячеек в строке. Для этого будет задавать в структуре struct Constants количество столбцов columnCount: и следить за тем, чтобы ширина ячейки width не была меньше, чем minWidthRation * bounds.width:

Теперь мы сможем вычислить предопределенную ширину ячейки predefinedWidth:

В этом вычислении predefinedWidth участвуют полная ширина коллекции boundsCollectionWidth, зазоры между ячейками gapItems, зазоры между секциями  gapSections и количество элементов коллекции  в строке Constants.columnCount. Значения boundsCollectionWidthgapItems и  gapSections рассчитываются…

… исходя из параметров коллекции Collection View, задаваемых на storyboard:

В свою очередь параметры, связанные с расположением элементов коллекции и заданные на storyboard, можно определить с помощью convenience переменной var  ……

А количество элементов коллекции  в строке Constants.columnCount можно задавать заранее, в дальнейшем (через секунду) мы сможем менять его с помощью жестов. Теперь в методе sizeForItemAt мы можем  использовать  predefinedWidth в качестве ширины width ячейки коллекции:

Запускаем приложение и получаем 3 ячейки в строке с одинаковой шириной width:

Поскольку предопределенная ширина ячейки коллекции predefinedWidth зависит от полной ширины коллекции collectionView?.bounds.width, которая меняется при изменении Size Class, то в этом случае необходимо заново позиционировать ячейки коллекции Collection View, используя новое значение предопределенной ширины predefinedWidth в методе sizeForItemAt. Мы можем напрямую попросить UICollectionViewFlowLayout коллекции UICollectionView сделать это, указав неправильность расположения элементов items в UICollectionViewFlowLayout с помощью метода flowLayout?.invalidateLayout(). В этом случае коллекция UICollectionView немедленно заново будет позиционировать ячейки, используя метод  sizeForItemAt.

Лучше всего это сделать в методе viewWillTransition(to size: , with:) :


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

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

Реализуйте жест pinch на вашей полной коллекции Collection View, который масштабирует  ширину width ваших ячеек cells (помните, что все ячейки cellsимеют одинаковую ширину width).

Добавляем распознавание жеста  pinch на Collection View в методе «жизненного цикла» viewDidLoad нашего Controller:

Обработка жеста pinch осуществляется Сontroller  с помощью метода zoom(_:), который имеет единственный параметр и должен быть помечен @objc:

Он управляет переменной scale, которая представляет собой масштаб отображения коллекции Collection View и которая, конечно, должна участвовать в вычислении предварительной ширины ячейки коллекции predefinedWidth:

При каждом изменении переменной масштаба scale мы аннулируем предыдущее расположение ячеек одной строкой кода …

flowLayout?.invalidateLayout()

… и заставляем коллекцию Collection View немедленно заново позиционировать ячейки, используя теперь уже ваш новые размеры ячеек в методе sizeForItemAt.

Запускаем приложение на симуляторе и используем жест pinch, удерживая клавишу option:

Соединяем пальцы и получаем значительно меньшую предварительную ширину ячейки коллекции predefinedWidth и, следовательно, большее количество ячеек в строке:

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

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

Когда пользователь “тапает” (жест tap) на ячейке cell в вашей коллекции Collection View, “переезжайте” (segue) на новый MVC, который представляет изображение в Scroll View, который заполняет полный MVC, так что пользователь может увеличивать (zoom in) и уменьшать (zoom out) масштаб изображения для исследования его в деталях. Это означает, что ваша коллекция Collection View будет вставлена внутрь Navigation Controller.

Используем MVC для показа изображения из демонстрационного примера Cassini Лекции 10. Сначала копируем из проекта  Cassini L10 в наш проект файл ImageViewController.swift с public API в виде URL загружаемого изображения imageURL:

Затем экранный фрагмент Image View Controller:

Создаем с помощью CTRL— перетягивания создаем Show Segue от ячейки к новому MVC 

… и называем его Show Image:

Для того, чтобы Show Segue начал работать, необходимо вставить наш основной MVC в Navigation Controller с помощью меню Editor -> Embed In -> Navigation Controller:

В результате на storyboard у нас 3 MVC :

Теперь мы можем кликнуть на ячейке коллекции …

… и подробно рассмотреть выбранное изображение:

Можно использовать жест pan, чтобы рассмотреть, например, правую часть изображения:

Или увеличить масштаб с помощью жеста pinch, чтобы рассмотреть гальдоньера:

Кнопка «Back» позволяет нам вернуться назад к коллекции изображений.

Пункты 1 и 5 обязательные

1. Создайте приложение, центральной частью которого является коллекция Collection View, который содержит изображения, “перетянутые” на него с помощью механизма Drag & Drop. Коллекция такого рода изображений называется “Image Gallery” (“Галерея изображений”).

5. Пользователь также должен иметь возможность реорганизовать элементы items в коллекции Collection View с помощью механизма Drag & Drop.

Сейчас в нашем приложении есть MVC, отображающий коллекцию изображений и управляющий этой коллекцией с помощью жестов. Теперь нам нужен внедрить в него механизм Drag & Drop, позволяющий пополнять эту коллекцию извне, из другого приложения, например, Safari c Google в качестве поисковой системы.

Реальные ворота, через которые нужно пройти, чтобы заработал Drag&Drop, — это так называемые “взаимодействия” interactions. Механизм Drag&Drop работает на  UIView. Точно также, как жесты (gestures), являются элементами UIView. Вы можете думать о Drag&Drop просто как о реально мощном жесте (gesture), если хотите.

Но в случае с Drag&Drop вы не будете добавлять к UIView распознавателя жеста (gesture recognizer) с помощью addGestureRecognizer, вместо этого вы добавляете к UIView “взаимодействие” dropInteraction или dragInteraction. Это выглядит практически точно также как добавление распознавателя жеста (gesture recognizer). UIView просто вызывает метод с именем addInteraction, который берет в качестве аргумента созданные вами dragInteraction или dropInteraction.

В свою очередь dragInteraction или dropInteraction создаются очень легко, у них всего один аргумент в инициализаторе, которым является делегат delegate. Затем происходит следующее: когда стартуют Drag или Drop или оба, у вас может быть view с обоими, то делегат theDelegate начинает получать некоторые сообщения. И вы должны реагировать на эти сообщения, если вы хотите участвовать в процессе Drag&Drop, причем реально очень легко реагировать на эти сообщения.

Давайте сначала реализуем «сброс» Drop в Мире коллекции Collection View. Это выглядит почти в точности как и в “Мире” НЕ Collection View. Единственная разница между Drop в “Мире” коллекции Collection View состоит в том, что Collection View помогает вам с indexPath элемента коллекции, куда вы хотите перетащить. Она знает, где находится ваш палец и интерпретирует это как indexPath элемента коллекции, куда вы “бросаете” (dropping) что-то в данный момент. Так что коллекции Collection View снабжает вас indexPath, а в остальном это абсолютно то же самое.

Каждый раз, когда вы хотите что-то “перетаскивать” в коллекцию Collection View, первое, что вы всегда должны делать в случае Drop взаимодействия, это установить себя, self, в качестве dropDelegate. Мы сделаем это в методе viewDidLoad:

Мы опять должны подняться в верхнюю часть нашего класса и подтвердить реализацию протокола UICollectionViewDropDelegate:

Как только мы добавили наш новый протокол, компилятор начал “жаловаться”, что мы этот протокол не реализовали.

Кликаем на кнопке Fix, и перед нами появятся обязательные методы этого протокола. В данном случае нам сообщают, что мы должны реализовать метод performDrop:

Имеет смысл реализовать метод performDrop, мы должны это сделать, иначе не произойдет “сброса” Drop. Давайте сделаем копирование этого кода и вставку его в нижнюю часть класса ImageGalleryCollectionViewController. В действительности я собираюсь реализовать метод performDrop в последнюю очередь, потому что есть пара других методов, которые необходимо реализовать для Drop части. Это canHandle и dropSessionDidUpdate:

Если мы реализуем эти два метода, то мы можем получить маленький зелененький “+”, когда будем перетаскивать наши изображения, а кроме того, нам не будут пытаться сбрасывать то, что мы не понимаем.

Давайте реализуем canHandle. У нас с вами версия метода canHandle, которая предназначается для коллекции Collection View. Нам нужно просто вернуть session.canLoadObjects (ofClass: UIImage.self), и это означает, что я принимаю “сброс” объектов этого класса в моей коллекции Collection View:


Но этого недостаточно. У нас есть обязательный пункт 2:

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

Разрешайте “сбрасывать” Drop только те элементы, у которых есть и UIImage изображения, и URL изображения.

Это касается «сброса» Drop изображения в мою коллекцию Collection View ИЗВНЕ. Если «сброс» Drop изображения происходит ВНУТРИ коллекции Collection View, когда пользователь реорганизует свои собственные элементы items с помощью механизма Drag & Drop, то достаточно одного изображения UIImage, и реализация  метода canHandle будет выглядеть вышеуказанным образом. Но если «сброс» Drop изображения происходит ИЗВНЕ, то мы должны обрабатывать только те перетаскивания Drag, которые представляют собой изображение UIImage, а также URL для этого изображения. В этом случае я верну true в методе canHandle только, если выполняется условие session.canLoadObjects(ofClass: NSURL.self) && session.canLoadObjects (ofClass: UIImage.self).

Мне осталось определить, имею ли я дело со «сбросом» ИЗВНЕ или ВНУТРИ. Я буду это делать с помощью вычисляемой константы isSelf, для вычисления которой я могу использовать такую вещь у Drop сессии session, как соответствующая локальная Drag сессия localDragSession. У этой локальной Drag сессии в свою очередь есть локальный контекст localContextи я буду исследовать его равенство моей коллекции collectionView. Если локальный контекст session.localDragSession?.localContext as? UICollectionView равен моей коллекции collectionView, то имеет место локальный «сброс» ВНУТРИ моей коллекции.

Правда ТИП у localContext будет Any, и мне необходимо сделать «кастинг» ТИПА Any с помощью as? UICollectionView.

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

Теперь при сбросе Drop я могу в методе dropSessionDidUpdate взглянуть на этот локальный контекст localContext и определить, следует ли выполнить «сброс» Drop копированием (.copy) или просто перемещением (.move). 

Для этого мне опять нужна вычисляемая константа isSelf, которая будет означать перемещение внутри коллекции collectionView или нет. Если это self реорганизация, то я выполняю перемещение .move, в противном случае я выполняю копирование .copy:

В этом методе мы должны вернуть Drop предложение, которое может иметь значение .copy или .move или .cancel, и это все возможности, которыми мы располагаем. Но вы видите, что здесь у нас ТИП UICollectionViewDropProposal для возвращаемого Drop предложение. Почему здесь указан другой ТИП, а не стандартный ТИП UIDropProposal?

Давайте вернем Drop предложение с помощью конструктора класса UICollectionViewDropProposal с дополнительным параметром intent для коллекции Collection View. Он сообщает о том, хотите ли вы сбрасываемое значение разместить внутри этой ячейки cell или вы хотите добавить новую ячейку cell. Видите разницу? Мы должны здесь сказать о нашем намерении. 

В нашем случае мы всегда хотим добавлять новую ячейку и параметр intent примет значение.insertAtDestinationIndexPath в противоположность .insertIntoDestinationIndexPath

Я пока еще не реализовала метод performDrop, но давайте взглянем на то, что уже может делать коллекция collectionView с этой маленькой порцией информации, которую мы ей предоставили.

Я перетаскиваю изображение из Safari, и у этого изображения появляется сверху зеленый знак «+», сообщающий о том, что наша Галерия Изображений готова принять и скопировать это изображение вместе с его URL:

Но если мы «сбросим» это изображение, оно не разместится в нашей Галерее, а просто вернется на прежнее место, потому что мы еще не реализовали метод performDrop. Я расположу код этого метода в более удобной для чтения длинных имен форме:

Вы видите, что у метода performDrop два аргумента.

Один — это просто коллекция collectionView, с которой мы работаем, а второй —  этот координатор coordinator. Координатор coordinator будет предоставлять нам всю информацию, которую необходимо нам знать для выполнения “сброса” Drop в коллекции collectionView.

Первое и наиболее важное, что нам сообщает координатор coordinator, это destinationIndexPath, то есть indexPath “пункта-назначения” “сброса” Drop, куда мы будем “сбрасывать” Drop.

Но он может быть равен nil, потому что вы можете перетащить элемент коллекции в ту часть коллекции collectionView, которая не является местом между какими-то уже существующими ячейками cells, так что он вполне может равняться nil. Если происходит именно эта ситуация, то я создаю IndexPath с 0-м элементом item и в 0-ой секции section.

Можно выбрать любой другой indexPath, но этот indexPath я использую по умолчанию.

Теперь мы знаем, где мы будем производить “сброс” Drop.

Мы должны пройти по всем элементам коллекции item в coordinator.items. Эти элементы coordinator.items имеют ТИП UICollectionViewDropItem и могут предоставить нам очень интересные куски информации. Например, если я смогу получить sourceIndexPath из item.sourceIndexPath, то я точно буду знать, что это перетаскивание Drag выполняется от самого себя, self, и источником перетаскивания Drag является элемент коллекции с indexPath равным sourceIndexPath :

Мне даже не надо смотреть на localСontext в этом случае, чтобы узнать, что это перетаскивание было сделано ВНУТРИ коллекции collectionView, здорово!

Теперь я знаю источник sourceIndexPath и “пункт-назначения” destinationIndexPath механизма Drag & Drop, и задача становится тривиальной. Локальный случай — самый простейший, и мы вернемся к нему позже, а сейчас рассмотрим более сложный случай «сброса» ИЗВНЕ, из другого приложения. 

Для этого в коде мы пишем else по отношению к sourceIndexPath. Если у нас нет sourceIndexPath, то это означает, что сбрасываемая вещь пришла откуда-то извне:

Если вы что-то “перетаскиваете” Drag ИЗВНЕ и “бросаете” Drop, то становится ли эта информация доступна мгновенно? Нет, вы выбираете данные из «перетаскиваемой» вещи АСИНХРОННО. А что, если выборка потребует 10 секунд? Чем будет заниматься в это время коллекция Сollection View? И здесь нам приходит на помощь подсказка №4 из Задания 5:

Наилучшей реализацией “сброса” (Dropping) в вашу коллекцию Collection View было бы использование местозаменителя Placeholder (как показано на Лекции), но это не требуется в Обязательных пунктах Задания. В некотором смысле, возможно. Это легче сделать с помощью местозаменителей Placeholders.

Вы размещаете в своей коллекции Collection View местозаменитель Placeholder, и коллекция Collection View управляет всем этим вместо вас, так что все, что вам нужно сделать, когда данные наконец будут выбраны, это попросить местозаменитель Placeholder вызвать его контекст placeholderContext и сообщить, что вы получили информацию, затем обновить свою Модель и контекст АВТОМАТИЧЕСКИ поменяет местами ячейку cell с местозаменителем Placeholder на одну из ваших ячеек cells, которая соответствует типу данных, которые вы получили.

Все эти действия мы производим путем создания контекста местозаменителя placeholderContext, который управляет местозаменителем Placeholder и который вы получаете из координатора coordinator, попросив “бросить” Drop элемент item на местозаменитель Placeholder. Я буду использовать инициализатор для контекста местозаменителя placeholderContext, который “бросает” dragItem на UICollectionViewDropPlaceholder:

Объект, который я собираюсь “бросить” Drop, это item.dragItem, где item —  это все еще элемент for цикла, так как мы можем “бросать” Drop множество объектов coordinator.items. Мы “бросаем” их один за другим. Итак, item.dragItem — это то, что мы перетаскиваем Drag. Следующим аргументом этой функции является местозаменитель, и я  создам его с помощью инициализатора UICollectionViewDropPlaceholder :

Для того, чтобы сделать это, мне нужно знать, ГДЕ я собираюсь вставлять местозаменитель Placeholder, то есть insertionIndexPath, а также идентификатор повторно используемой ячейки reuseIdentifier.

Аргумент insertionIndexPath, очевидно,  равен destinationIndexPath, это IndexPath для размещения перетаскиваемого объекта, он рассчитывается в самом начале метода performDropWith.

Теперь посмотрим на идентификатор повторно используемой ячейки reuseIdentifier. ВЫ должны решить, какого типа ячейка cell является вашим местозаменитель Placeholder. У координатора coordinator нет “заранее укомплектованной” ячейки cell для местозаменителя Placeholder . Именно ВЫ должны принять решение об этой ячейки cell. Поэтому запрашивается идентификатор повторно используемой ячейки reuseIdentifier с вашей storyboard для того, чтобы ее можно было использовать как ПРОТОТИП.

Я назову его “DropPlaceholderCell”, но в принципе, я мог назвать его как угодно. Это просто строка String, которую я собираюсь использовать на моей storyboard для создания этой вещи.

Возвращаемся на нашу storyboard и создаем ячейку cell для местозаменителя Placeholder . Для этого нам нужно просто выбрать коллекцию Collection View и инспектировать ее. Выбираем самое первое поле Items, и в нем я изменяю 1 на 2. Это сразу же создает нам вторую ячейку, которая является точной копией первой.

Выделяем нашу новую ячейку ImageCell, устанавливаем идентификатор “DropPlaceholderCell”, удаляем оттуда все UI элементы, включая Image Gallery, так как этот ПРОТОТИП используется тогда, когда изображение еще не поступило. Добавляем туда из Палитры Объектов новый индикатор активности Activity Indicator, он будет вращаться, давая понять пользователям, что я ожидаю некоторых “сброшенных” данных. Изменим также цвет фона Background , чтобы понимать, что при «сбросе» изображений ИЗВНЕ работает именно эта ячейка cell как ПРОТОТИП:

Кроме того ТИП новой ячейки не должен быть ImageCollectionVewCell, потому что в ней не будет изображений. Я сделаю эту ячейку обычной ячейкой ТИПА UIСollectionCiewCell, так как нам не нужны никакие Outlets для управления:

Давайте сконфигурируем индикатор активности Activity Indicator таким образом, чтобы он начал анимировать с самого начала, и мне не пришлось бы ничего писать в коде, чтобы запустить его. Для этого нужно кликнуть на опции Animating:

И это все. Итак, мы сделали все установки для этой ячейки DropPlaceholderCell, возвращаемся в наш код. Теперь у нас есть прекрасный местозаменитель Placeholder, готовый к работе. Все, что нам осталось сделать, это получить данные, и когда данные будут получены, мы просто скажем об этом контексту placeholderСontext, он поменяет местами местозаменителя Placeholder и данные, а мы сделаем изменения в Модели.

 

Я собираюсь “загрузить” ОДИН объект, которым будет мой item. Я использую код item.dragItem.itemProvider с поставщиком itemProvider, который обеспечит меня данными элемента item АСИНХРОННО. Ясно, что если подключился itemProvider, то объект “сброса” item мы получаем за пределами данного приложения. Далее следует метод loadObject (ofСlass: UIImage.self) (в единственном числе):

Это конкретное замыкание выполняется НЕ на main queue. И, к сожалению, нам пришлось переключиться на main queue с помощью DispatchQueue.main.async {} для того, чтобы «поймать» соотношение сторон изображения в локальную переменную aspectRatioLocal.

Мы используем подсказку № 3:

Когда происходит “сброс” Drop, вам придется забрать как Aspect ratio (из   UIImage), так и URL (из NSURL) перед тем, как вы сможете добавить элемент item. Вы могли бы сделать это просто с помощью пары локальных переменных, которые захватываются замыканиями, используемыми для загрузки данных механизма Drag & Drop.

Мы действительно ввели две локальные переменные imageURLLocal и aspectRatioLocal …

… и будем ловить их при загрузки изображения image и URL url:

Если обе локальные переменные не равны nil, мы попросим контекст местозаменителя placeholderСontext с помощью метода commitInsertion изменить нашу Модель imageCollection, что повлечет за собой изменение UI

В этом выражении у нас есть insertionIndexPath — это indexPath для вставки, и мы изменяем нашу Модель imageCollection. Это все, что нам нужно сделать, и этот метод АВТОМАТИЧЕСКИ заменит местозаменитель Placeholder на ячейку cell путем вызова нормального метода cellForItemAt.

Заметьте, что insertionIndexPath может сильно отличаться от destinationIndexPath.

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

ВСЕГДА используйте здесь insertionIndexPath, и только его используйте для обновления вашей Модели. Как мы обновляем нашу Модель?

Мы вставим в массив imageCollection структуру imageModel, составленную из соотношение сторон изображения aspectRatioLocal и URL изображения imageURLLocal, которые вернул нам соответствующий provider.

Это обновляет нашу Модель imageCollection, а метод commitInsertion делает за нас все остальное. Больше вам не нужно делать ничего дополнительного, никакие вставки, удаления строк, ничего из этого. И, конечно, поскольку мы находимся в замыкании, то нам нужно добавить self..

Если мы по некоторым причинам не смогли получить соотношение сторон изображения aspectRatioLocal и URL изображения imageURLLocal из соответствующего provider, возможно, была получена ошибка error вместо provider, то мы должны дать знать контексту placeholderContext, что нужно уничтожить этот местозаменитель Placeholder, потому что мы все равно мы не сможем получить других данных:

Необходимо иметь ввиду одну особенность URLs, которые приходят из мест наподобие Google, в действительности они нуждаются в незначительных преобразованиях для получения “чистого” URL для изображения. И здесь нам пригодится подсказка № 2:

Подсказка № 2. ОЧЕНЬ ВАЖНО: URLs, которые приходят из мест наподобие Google, в действительности нуждаются в незначительных преобразованиях для получения “чистого” URL для изображения. Вспомогательный код, расположенный в файле Utilities.swift, который поставляется вместе с демонстрационным приложением EmojiArt, имеет простое расширение extension класса URL для получения этого “чистого URL, которое называется imageURL. По существу: каждый раз, когда вы выбираете из интернета изображение, делайте это с помощью imageURL из класса URL. Если вы попытаетесь выбрать напрямую из этого URL, в большинстве случаев это не будет работать.

Поэтому при получении URL изображения мы используем свойство imageURL из класса URL:

И это все, что нужно сделать, чтобы принять что-то внутрь коллекции Collection View откуда-то извне.

Давайте посмотрим это в действии. Запускаем одновременно в многозадачном режиме наше приложение ImageGallery и Safari  с поисковой системой Google. В  Google  мы ищем изображения на тему «Рассвет» (sunrise). В Safari уже встроен Drag & Drop механизм, поэтому мы можем выделить одно из этих изображений, долго удерживать его, немного сдвинуть и перетащить в нашу Галерею Изображений.

Наличие зеленого плюсика «+» говорит о том, что наше приложение готово принять стороннее изображение и скопировать его в свою коллекцию на указанное пользователем место. После того, как мы «сбросим» его, требуется некоторое время на загрузку изображения, и в это время работает Placeholder:

После завершения загрузки, «сброшенное» изображение размещается на нужном месте, а Placeholder исчезает:

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

Наш viewDidLoad становится очень простым: в нем мы делаем наш ImageGalleryCollectionViewController Drop делегатом и добавляем распознаватель жеста pinch :

Теперь мы стартуем с пустой коллекции…

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

… присходит загрузка изображения и Placeholder работает…

и изображение появляется на нужном месте:

Мы можем полностью наполнить нашу коллекцию ИЗВНЕ:

Теперь перед нами стоит задача предоставления пользователю возможности внутренней реорганизации элементов коллекции items с помощью механизма Drag & Drop. И мы плавно переходим к обязательному пункту 5 (еще раз его напомним).

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

Пользователь также должен иметь возможность реорганизовать элементы items в коллекции Collection View с помощью механизма Drag & Drop.

Каждый раз, когда вы хотите что-то “перетаскивать” в коллекции Collection View, первое, что вы всегда должны делать, также как и в случае Drop взаимодействия, вы должны установить себя, self, в качестве dragDelegate:

И, конечно, в самом верху класса ImageGalleryCollectionViewController мы должны сказать, что “Да”, мы реализуем протокол UICollectionViewDragDelegate:

Как только я это сделаю, компилятор начинает “жаловаться”, мы кликаем на красном кружочке и нас спрашивают: “Хотите добавить обязательные методы протокола UICollectionViewDragDelegate?:

Я отвечаю: “Конечно, я хочу. Что мне нужно делать?” И я кликаю на кнопке Fix:

Мне говорят, что я должна реализовать метод itemsForBeginning. Это та вещь, которая скажет Drag системе, ЧТО мы перетаскиваем. Мы должны обеспечить эти элементы перетаскивания [UIDragItem].

Давайте сделаем это, но сначала переместим метод  itemsForBeginning с верхней части класса вниз туда, где находится код, относящийся к  UICollectionViewDragDelegate.

Как мы будем реализовывать itemsForBeginning? Первое, что я сделаю, это укажу локальный контекст localContext  для сессии session перетаскивания Drag равным моей коллекции collectionView. Переменная с именем localContext — это просто нечто в Drag сессии session, что позволяет тем, кто производит “сброс” (Drop) знать: “Эй, это локальное перетаскивание Drag и это его контекст localContext.” Поскольку это перетаскивание Drag происходит из коллекции collectionView, я использую collectionView в качестве этого контекста:

Теперь при сбросе Drop я могу в методе dropSessionDidUpdate взглянуть на этот локальный контекст localContext и определить, следует ли сделать мое перетаскивание Drag копированием (.copy) или просто перемещением (.move).

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

Аналогичная ситуация с методом canHandle делегата  UICollectionViewDropDelegate: при «локальном» перемещении мы работаем с объектами UIImage, а при перемещении ИЗВНЕ — с UIImage и URL :

Итак, возвращаемся к перетягиванию Drag элементов коллекции Collection View и методам делегата UICollectionViewDragDelegate.

Мы замечаем, что в основном методе itemsForBeginning коллекция Collection View добавила indexPath, это подскажет нам, какой элемент коллекции, какой indexPath, мы собираемся перетаскивать, что действительно  для нас очень удобно. Я создам небольшую private функцию dragItems (at: indexPath), в которой indexPath — это секция section и элемент item. Она возвращает нужный нам массив [UIDragItem]:

Это все, что мы должны сделать, чтобы начать перетаскивать Drag. Для нас очень-очень легко реализовать этот метод, потому что мы будем перетаскивать Drag то, является изображением UIImage, а UIImage является itemProvider также, как и NSURL, NSStringNSAttributedString.

Все, что нам нужно сделать, это получить изображение image, которую мы хотим перетаскивать и которое мы затем вернем как массив [UIDragItem].

У меня есть indexPath, с помощью которого нахожу пользовательскую (Custom) ячейку itemCell, смотрю на ее Outlet imageGallery и получаю его изображение image. Давайте выразим эту идею парой строк кода:

Сначала я запрашиваю мою коллекцию сollectionView о ячейки cell для элемента item, соответствующего этому indexPath. Метод cellForItem (at: IndexPath) для коллекции Collection View подобен методу cellForRow (at: IndexPath) для таблицы Table View. Они работают только для видимых (visible) ячеек, но, конечно, они будут работать в нашем случае, ведь я перетаскиваю Drag элемент коллекции, находящийся на экране, и он является видимым. Итак, я получила прямо в коде ячейку itemCell, которую перетаскиваю.

Далее я должна применить оператор as? к этой ячейке, чтобы она имела ТИП моего пользовательского subclass. И если это работает, надеюсь, что это сработает, то я получаю Outlet imageGallery, у которого беру его изображение image. Если все это работает, то я просто “захватила” изображение image для этого indexPath.

Теперь, когда у меня есть изображение image, все, что мне необходимо сделать, это создать один из этих UIDragItems, используя полученное image в качестве itemProvider, то есть вещи, которая обеспечивает нас данными.

Я могу создать dragItem с помощью конструктора UIDragItem, который берет в качестве аргумента itemProvider:

Затем мы создаем itemProvider для image также с помощью конструктора NSItemProvider. Существует несколько конструкторов для NSItemProvider, но среди них есть один действительно замечательный — NSItemProvider (object:NSItemProviderWriting):

Этому конструктору NSItemProvider вы просто даете объект object, и он знает, как сделать из него itemProvider наподобие UIImage или NSAttributedString. В качестве такого объекта object я даю изображение image, которое я получил из ячейки itemCell. И это все. Мы создали dragItem и должны вернуть его как массив, имеющий один элемент.

Но прежде, чем я верну dragItem, я собираюсь сделать еще одну вещь, я собираюсь установить переменную localObject для dragItem, равную элементу коллекции imageCollection, соответствующему данному indexPath.

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

Это своего рода “короткое замыкание” при локальном перетаскивании Drag.

Далее я возвращаю массив, состоящий из одного элемента dragItem.

Между прочим, если я не смог получить по каким-то причинам dragItem для этой ячейки imageCell, то я возвращаю пустой массив [ ], это означает, что нет ничего, что будет перетаскиваться Drag, и это будет известно уже с самого начала процесса перетаскивания Drag.

Начав перетаскивание Drag, вы можете добавлять еще больше элементов items к этому перетаскиванию, просто выполнив жест tap на них. В результате вы можете перетаскивать Drag множество элементов за раз. И это легко реализовать с помощью другого метода делегата  UICollectionViewDragDelegate, очень похожего на метод itemsForВeginning, метода с именем itemsForAddingTo. Метод itemsForAddingTo выглядит абсолютно точно также, как метод itemsForВeginning, и возвращает абсолютно ту же самую вещь, потому что он также дает нам indexPath того, на чем “тапнул” пользователь в процессе перетаскивания Drag, и мне достаточно получить изображение из ячейке, на которой “тапнули” и вернуть его.

И это все, что нам необходимо для  перетаскивания Drag. Запускаем приложение и смотрим. Как всегда наше приложение стартует с пустой Галереи Изображений, мы наполняем Галерею «Рассвет» (sunrise), перетягивая изображения из поисковика Google в Safari:

Теперь попробуем переместить какое-нибудь изображение внутри коллекции и вы видите, что для этого изображения будет подготавливаться соответствующее место…

Но я не могу его “бросить” Drop в моей коллекции Collection View, потому что я еще не реализовала «локальный» механизм Drop.

Когда происходит “сброс” Drop, то в методе performDrop мы должны обновить нашу Модель, которой является список imageCollection изображений с их URL и Aspect Ratio

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

… и мы должны обновить коллекцию collectionView.

Мы уже отмечали, что у нас происходит два различных типа “сброса” Drop.

Есть “сброс” Drop, который идет из моей коллекции collectionView. В этом случае я должна выполнить “сброс” Drop элемента коллекции на новом месте и и убрать его со старого места, потому что в этом случае я перемещаю (.move) этот элемент коллекции UIImage.

Есть “сброс” Drop, который идет из другого приложения. В этом случае я копирую (.copy) изображение вместе с его URL, пришедшее ИЗВНЕ.

Теперь нам нужно сделать Drop часть для моей коллекции Collection View. У нас уже есть все методы Drop делегата, нам осталось только дополнить «локальную» часть метода performDrop. Eсли я смогу получить sourceIndexPath из item.sourceIndexPath, то я точно буду знать, что это перетаскивание Drag выполняется от самого себя, self, и источником перетаскивания Drag является элемент коллекции с indexPath равным sourceIndexPath :

Мне даже не надо смотреть на localСontext в этом случае, чтобы узнать, что это перетаскивание было сделано внутри меня — коллекции collectionView, здорово!

Теперь я знаю источник sourceIndexPath и “пункт-назначения” destinationIndexPath механизма Drag&Drop, и задача становится тривиальной. Все, что мне необходимо сделать, это обновить Модель так, чтобы источник и “пункт-назначения” поменялись местами, а затем обновить коллекцию collectionView, в которой нужно будет убрать элемент коллекции с sourceIndexPath и добавить его в коллекцию с destinationIndexPath.

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

В нашем случае мне не понадобиться даже localObject, который я “припрятала” ранее, когда создавала dragItem и который я могу заимствовать теперь у «перетаскиваемого» элемента коллекции item. Он нам понадобиться при «сбросе «изображений в «мусорный бак». Сейчас мне достаточно двух IndexPathes: источника sourceIndexPath и “пункта-назначения” destinationIndexPath:

 

Сначала я получаю  информацию imageInfo об изображении на старом месте, убирая его  оттуда. А затем вставляю в массив imageCollection, это моя Модель, информацию imageInfo об изображении с новым индексом destinationIndexPath.item. Вот так, я обновила мою Модель:

 

 

Теперь я должна обновить саму коллекцию collectionView. Очень важно понимать, что я не хочу перегружать все данные в моей коллекции collectionView с помощью reloadData() в середине процесса «перетаскивания» Drag, потому что это переустанавливает целый “Мир”, что очень плохо, НЕ ДЕЛАЙТЕ ЭТОГО. Вместо этого я собираюсь убирать и вставлять элементы items по отдельности:

 

 

Я удалила элемент коллекции collectionView с sourceIndexPath и вставила новый элемент коллекции с destinationIndexPath. Выглядит так, как будто бы этот код прекрасно работает, но в действительности, этот код может “обрушить” ваше приложение. Причина заключается в том, что вы делаете многочисленные изменения в вашей коллекции collectionView, а в этом случае каждый шаг изменения  коллекции нужно нормально синхронизировать с Моделью, что в нашем случае не соблюдается, так как мы выполняем обе операции одновременно: удаление и вставку. Следовательно, коллекция будет находиться в НЕ синхронизированном состоянии с Моделью.

Но есть реально крутой способ обойти это, который состоит в том, что коллекция Collection View имеет метод с именем performBatchUpdates.

Метод performBatchUpdates имеет замыкание (closure)…

… и внутри этого замыкания я могу разместить любое число этих deleteItems, insertItems, moveItems и все, что я хочу делать:

 

Теперь deleteItems и insertItems будут выполняться как одна операция и никогда не будет наблюдаться отсутствие синхронизации вашей Модели. У метода performBatchUpdates есть также прекрасное замыкание completion, которое в данном случае нам не понадобится.

И, наконец, последняя вещь, которую нам необходимо сделать, это попросить координатор coordinator выполнить “сброс” Drop:

 

Причина, по которой нам необходимо это делать, заключается в том, что мы хотим анимировать то, как происходит “сброс” Drop. Поэтому мы вызываем функцию координатора coordinator.drop. Именно эта функция и заставляет осуществиться “сброс” Drop. Как только вы поднимаете палец от экрана, изображение перемещается, все происходит в одно и то же время : “сброс”, исчезновение изображения в одном месте и появление в другом.

Давайте запустим приложение и посмотрим на это. Допустим, мы наполнили нашу Галерею изображений «Закат» изображениями из Google:

Давайте переместим самое левое изображение в конец первой строки:

И «бросим» его:

Ура! Все работает!

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

19. Если вы перетаскиваете Drag URL, который не является безопасным (то есть у него http:// вместо https://), то по умолчанию он не будет принят UIImage. Вы можете изменить эту ситуацию в вашем Info.plist файле. Кликните на Info.plist, затем CTRL-кликните на его фоне и выберите опцию Add Row из контекстного меню, которое “всплывает”, затем прокручивайте список пока не наткнетесь на App Transport Security Settings, кликните на маленьком треугольнике слева на новой строке, это развернет треугольник “носиком” вниз, затем кликаем на кнопке с + чтобы добавить подстроку, выбираем Allow Arbitrary Loads и устанавливаем этот атрибут в YES. После того, как вы все это проделаете, у вас должна быть запись в вашем  Info.plist файле наподобие следующей…


Мы можем проверить также подсказку №6:

Совершенно очевидно, что, если вы выполнили правильную многопоточность при “сбросе” ряда больших изображений, а затем увеличиваете их масштаб,  так что у вас в огромном количестве появляются повторно используемые ячейки cells, то вы должны увидеть кучу вращающихся “колесиков”. Именно это мы и делаем, когда оцениваем вашу работу!

Применяем жест pinch для изменения масштаба…

… и видим множество вращающихся «колесиков»:

Последним штрихом для нашей Галерии изображений реализуем подсказку № 14 :

Если результатом выборки по данному URL является недопустимое (valid) изображение, возможно, вы захотите разместить какой-нибудь индикатор этого в результирующей ячейке коллекции Collection View. Может быть, нахмуренное лицо  или отметить как-то этот эффект для пользователя? Просто пустое пространство в вашей коллекции Collection View может немного запутать пользователя. Но впрочем, все на ваше усмотрение.

Займемся этим в классе ImageCollectionViewCell. Если нам не удалось получить изображение image и разместить его в ячейке коллекции Collection View, то мы сформируем изображение из строки, содержащей текст «Error » и эмоджи  с помощью метода emojiToImage() строки String и «затеним» его с помощью метода applyBlurEffect() изображения UIImage:

Метод emojiToImage() размещен в расширении extension строки String:

Метод applyBlurEffect() размещен в расширении extension изображения UIImage:

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

Вполне удовлетворительно, но нам бы не хотелось, чтобы изображение с сообщением об ошибке повторяло Aspect Ration этого ошибочного изображения, потому что в этом случае текст вместе с эмоджи будет растягиваться или сжиматься, нам бы хотел, чтобы оно было нейтральным — квадратным.

Для этого воспользуемся трюком, который профессор показывал на Лекции 13 для возвращения введенных новых эмоджи в коллекцию Collection View.  Я узнаю о том, что у меня ошибочное изображение и мне нужно изменить Aspect Ration этого изображения только в классе ImageCollectionViewCell, само новое значение Aspect Ration необходимо передать Модели imageCollection коллекции сollectionView и заставить сработать метод sizeForItemAt этой коллекции. У ячейки UICollectionViewCell нет указателя на свою коллекцию Collection View, нет такого API у класса UICollectionViewCell. Это интересная задача и большинство людей стали бы решать эту задачу, пытаясь найти каким-то образом свою коллекцию сollectionView и “поговорить” с ней. Но на самом деле есть значительно более легкий путь сделать это — с помощью замыканий (closures).

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

Когда мы получаем ошибочное изображение и мне нужно изменить Aspect Ration этого изображения, я вызываю эту функцию changeAspectRatio? ():

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

Теперь я могу установить ячейке imageСell замыкание changeAspectRatio, это будет замыкание, в котором мы можем делать все, что угодно, в том случае, если обнаружим ошибочное изображение с Aspect Ration отличным от 1. В этом замыкании я устанавливаю Aspect Ration равным 1 и одной строкой кода …

flowLayout?.invalidateLayout()

… немедленно заново позиционирую ячейки с  использованием нового размера itemSize. Конечно, я должна здесь ЯВНО использовать self с “точкой”:

Но каждый раз, когда мы это делаем, мы должны взять паузу и подумать, а не создаст ли это “циклическую ссылку память” (memory cycle) или это происходит из-за того, что у нас многопоточность? В данном случае у нас нет проблем с многопоточностью, которая возникает, когда ячейки cells прокручиваются вперед и назад  (мы решили ее в классе ImageCollectionViewCell. Но здесь есть “циклическая ссылка памяти” (memory cycle). Потому что self — это наш View Controller, и он указывает на нашу коллекцию Collection View. Наша коллекция Collection View, конечно, указывает на свои ячейки cells. Наша ячейка imageCell указывает на это замыкание (closure). И это замыкание, как мы видим, указывает обратно на наш View Controller. Они указывают друг на друга. Так что нам необходимо прервать эту “циклическую ссылку памяти” (memory cycle) с помощью [weak self] in, и это заставит нас заменить self на self? внутри нашего замыкания.

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

Теперь изображение с сообщением об ошибке имеет квадратную форму и текст и эмоджи не искажаются.

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

Продолжение следует…

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

  1. Татьяна, подскажите, пожалуйста. В лекциях Пол показывал, как можно изменять размеры constraints через код, автоматически подгоняя размер scrollView к imageView. Пытался проделать подобный маневр, но autolayout ругается и никак не позволяет отцентровать изображение. В чем может быть проблема? Шаги были выполнены 1 в 1 как на лекции.
    https://youtu.be/xkpuJejkWUI?t=557 — речь идет об этом моменте лекции.

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