Drag & Drop в ваших iOS приложениях

в 6:36, , рубрики: Collection View, drag and drop, ios development, iOS разработка, nscache, NSItemProvider, perfornDrop, swift, UIDragDelegate, UIDropDelegate, UserDefaults, xcode, Программирование, разработка мобильных приложений, разработка под iOS

Drag & Drop в ваших iOS приложениях - 1

Механизм Drag & Drop, работающий в iOS 11 и iOS 12, — это способ графического асинхронного копирования или перемещения данных как внутри одного приложения, так и между разными приложениями. Хотя этой технологии лет 30, она стала в буквальном смысле «прорывной» технологией на iOS благодаря тому, что при перетаскивании чего-либо в iOS, multitouch позволяет свободно взаимодействовать с остальной частью системы и набирать данные для сброса из разных приложений.

iOS делает возможным захват несколько элементов сразу. Причём они необязательно должны быть в удобной доступности для выбора: можно взять первый объект, потом перейти в другое приложение и захватить что-нибудь ещё — все объекты будут собираться в «стопку» под пальцем. Потом вызвать на экран универсальный док, открыть там любое приложение и захватить третий объект, а затем перейти на экран с с запущенными приложениями и, не отпуская объекты, сбросить их в одну из открытых программ. Такая свобода действий возможна на iPad, на iPhone зона действия Drag & Drop в iOS ограничена рамками одного приложения.

В большинство популярных приложений (Safary, Chrome, IbisPaint X, Mail, Photos, Files и т.д.) уже встроен механизм Drag & Drop. В дополнение к этому Apple предоставила в распоряжение разработчиков очень простой и интуитивный API для встраивания механизма Drag & Drop в ваше приложение. Механизм Drag & Drop, точно также, как и жесты, работает на UIView и использует концепцию «взаимодействий» Interactions, немного напоминающих жесты, так что вы можете думать о механизме Drag & Drop просто как о реально мощном жесте.

Его, также как и жесты, очень легко встроить в ваше приложение. Особенно, если ваше приложение использует таблицу UITableView или коллекцию UICollectionView, так как для них API Drag & Drop усовершенствован и поднят на более высокий уровень абстракции в том плане, что коллекция Collection View сама помогает вам с indexPath элемента коллекции, который вы хотите «перетаскивать» Drag. Она знает, где находится ваш палец и интерпретирует это как indexPath элемента коллекции, который вы “перетаскиваете” Drag в настоящий момент или как indexPath элемента коллекции, куда вы “cбрасываете” Drop что-то. Так что коллекция Collection View снабжает вас indexPath, а в остальном это абсолютно тот же самый API Drag & Drop, что и для обычного UIView.

Процесс Drag & Drop на iOS имеет 4 различных фазы:

Lift (подъем)

Lift (подъем) — это когда пользователь выполняет жест long press, указывая элемент, который будет «перетаскиваться и сбрасываться». В этот момент формируется очень легковесный так называемый «предварительный просмотр» (lift preview) указанного элемента, а затем пользователь начинает перемещать (Dragging) свои пальцы.

Drag & Drop в ваших iOS приложениях - 2

Drag (перетаскивание)

Drag (перетаскивание) — это когда пользователь перемещает объект по поверхности экрана. В процессе этой фазы «предварительный просмотр» (lift preview) для этого объекта может модифицироваться (появляется зеленый плюсик "+" или другой знак)…

Drag & Drop в ваших iOS приложениях - 3

… разрешено также некоторое взаимодействие с системой: можно кликнуть на каком-то другом объекте и добавить его к текущей сессии «перетаскивания»:

Drag & Drop в ваших iOS приложениях - 4

Drop (сбрасывание)

Drop (сбрасывание) происходит, когда пользователь поднимает палец. В этот момент могут произойти две вещи: либо Drag объект будет уничтожен, либо произойдет «сброс» Drop объекта в месте назначения.

Drag & Drop в ваших iOS приложениях - 5

Data Transfer (передача данных)

Если процесс «перетаскивания» Drag не был аннулирован и состоялся «сброс» Drop, то происходит Data Transfer (передача данных), при которой «пункт сброса» запрашивает данные у «источника», и происходит асинхронная передача данных.

В этой обучающей статье на примере демонстрационного приложения «Галерея Изображений», заимствованного из домашних заданий стэнфордского курса CS193P, мы покажем, как легко можно внедрить механизм Drag & Drop в ваше iOS приложение.
Мы наделим коллекцию Collection View способностью наполнять себя изображениями ИЗВНЕ, а также реорганизовывать ВНУТРИ себя элементы с помощью механизма Drag & Drop. Кроме того, этот механизм будет использован для сброса ненужных элементов коллекции Collection View в «мусорный бак», который является обычным UIView и представлен кнопкой на навигационной панели. Мы также сможем делиться с помощью механизма Drag & Drop собранными в нашей Галерее изображениями с других приложениями, например, с «Заметками» (Notes или Notability) или с почтой Mail или с библиотекой фотографий (Photo).

Но прежде чем сфокусироваться на внедрении механизма Drag & Drop в демонстрационное приложение «Галерея Изображений», я очень кратко пройдусь по его основным составным частям.

Возможности демонстрационного приложения «Галерея изображений»

Пользовательский интерфейс (UI) приложения «Галерея изображений» — очень прост. Это «экранный фрагмент» Image Gallery Collection View Controller, вставленный в Navigation Controller:

Drag & Drop в ваших iOS приложениях - 6

Центральной частью приложения безусловно является Image Gallery Collection View Controller, который поддерживается классом ImageGalleryCollectionViewController с Моделью Галереи Изображений в виде переменной var imageGallery = ImageGallery():

Drag & Drop в ваших iOS приложениях - 7

Модель представлена структурой struct ImageGallery, содержащей массив изображений images, в котором каждое изображение описывается структурой struct ImageModel, содержащей URL url местоположения изображения (мы не собираемся хранить само изображение) и его соотношение сторон aspectRatio:

Drag & Drop в ваших iOS приложениях - 8

Наш ImageGalleryCollectionViewController реализует DataSource протокол:

Drag & Drop в ваших iOS приложениях - 9

Пользовательская ячейка коллекции cell содержит изображение imageView: UIImageView! и индикатор активности spinner: UIActivityIndicatorView! и поддерживается пользовательским subclass ImageCollectionViewCell класса UICollectionViewCell:

Drag & Drop в ваших iOS приложениях - 10

Public API класса ImageCollectionViewCell — это URL изображения imageURL. Как только мы его устанавливаем, наш UI обновляется, то есть асинхронно выбираются данные для изображения по этому imageURL и отображаются в ячейке. Пока идет выборка данных из сети, работает индикатор активности spinner, показывающий, что мы в процессе выборки данных.

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

Drag & Drop в ваших iOS приложениях - 11

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

Тем не менее, в случае многопоточности вы должны принять во внимание, что ячейки cells в коллекции Collection View являются повторно-используемыми благодаря методу dequeueReusableCell. Каждый раз, когда ячейка (новая или повторно-используемая) попадает на экран, запускается асинхронно загрузка изображения из сети (в это время крутится «колесико» индикатора активности spinner).

Как только загрузка выполнена и изображение получено, происходит обновление UI этой ячейки коллекции. Но мы не ждем загрузки изображения, мы продолжаем прокручивать коллекцию и примеченная нами ячейка коллекции уходит с экрана, так и не обновив свой UI. Однако снизу должно появиться новое изображение и эта же ячейка, ушедшая с экрана, будет использована повторно, но уже для другого изображения, которое, возможно, быстро загрузится и обновит UI. В это время вернется ранее запущенная в этой ячейки загрузка изображения и обновит экран, что приведет к неправильному результату. Это происходит потому, что мы запускаем разные вещи, работающие с сетью, в разных потоках. Они возвращаются в разное время.

Как мы можем исправить ситуацию?
В пределах используемого нами механизма GCD мы не можем отменить загрузку изображения ушедшей с экрана ячейки, но мы можем, когда приходят  из сети наши данные imageData, проверить URL url, который вызвал загрузку этих данных, и сравнить его с тем, который пользователь хочет иметь в этой ячейки в данный момент, то есть imageURL. Если они не совпадают, то мы не будем обновлять UI ячейку и подождем нужных нам данных изображения:

Drag & Drop в ваших iOS приложениях - 12

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

Если выборку данных изображения не удалось выполнить, то формируется изображение с сообщением об ошибке в виде строки «Error» и эмоджи с «нахмуренное лицо». Просто пустое пространство в нашей коллекции Collection View может немного запутать пользователя:

Drag & Drop в ваших iOS приложениях - 13

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

Drag & Drop в ваших iOS приложениях - 14

Мы должны сообщить об этом пожелании нашему Controller, чтобы он исправил в своей Модели imageGallery соотношение сторон aspectRatio для соответствующего indexPath. Это интересная задача, есть много путей ее решения, и мы выберем наиболее легкий из них — использование Optional замыкания (closure) var changeAspectRatio: (() -> Void)?. Оно может равняться nil и его не нужно устанавливать, если в этом нет необходимости:

Drag & Drop в ваших iOS приложениях - 15

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

Drag & Drop в ваших iOS приложениях - 16

Подробности можно посмотреть здесь.

Для показа изображений с правильным aspectRatio используется метод sizeForItemAt делегата UICollectionViewDelegateFlowLayout:

Drag & Drop в ваших iOS приложениях - 17

Помимо коллекции изображений Collection View, на нашем UI мы разместили на навигационной панели кнопку Bar Button c пользовательским изображением GarbageView, содержащим «мусорный бак» в качестве subview:

Drag & Drop в ваших iOS приложениях - 18

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

Drag & Drop в ваших iOS приложениях - 19

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

Drag & Drop в ваших iOS приложениях - 20

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

Drag & Drop в ваших iOS приложениях - 21

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

Drag & Drop в ваших iOS приложениях - 22

Это начальный вариант демонстрационного приложения «Галерея изображений», оно находится на Github в папке ImageGallery_beginning. Если вы запустите этот вариант приложения «Галерея изображений», то увидите результат работы приложения на тестовых данных, которые мы впоследствии удалим и будем заполнять «Галерею изображений» исключительно ИЗВНЕ:

Drag & Drop в ваших iOS приложениях - 23

План по внедрению механизма Drag & Drop в наше приложение состоит в следующем:

  1. cначала мы наделим нашу коллекцию изображений Collection View способностью «перетягивать» Drag ИЗ нее изображения UIImage как вовне, так и локально,
  2. затем мы научим нашу коллекцию изображений Collection View принимать «перетянутые» Drag извне или локально изображения UIImage,
  3. мы также научим наше GarbageView с кнопкой «мусорного бака» принимать «перетянутые» из локальной коллекции Collection View изображения UIImage и удалять их из коллекции Collection View

Если вы пройдете до конца этой обучающей статьи и выполните все необходимые изменения кода, то получите окончательную версию демонстрационного приложения «Галерея изображений», в которую внедрен механизм Drag & Drop. Она находится на Github в папке ImageGallery_finished.

Работоспособность механизма Drag & Drop в вашей коллекции Collection View обеспечивается двумя новыми делегатами.
Методы первого делегата, dragDelegate, настроены на инициализацию и пользовательскую настройку «перетаскиваний» Drags.
Методы <uвторого делегата, dropDelegate, завершают «перетаскивания» Drags и, в основном, обеспечивают передачу данных (Data transfer) и пользовательскую настройку анимаций при «сбросе» Drop, а также другие подобные вещи.

Важно заметить, что оба эти протокола абсолютно независимые. Вы можете использовать один или другой протокол, если вам нужно только «перетягивание» Drag или только «сброс» Drop, но вы можете использовать сразу оба протокола и выполнять одновременно и «перетягивание» Drag, и «сброс» Drop, что открывает дополнительные функциональные возможности механизма Drag & Drop по изменению порядка элементов в вашей коллекции Collection View.

Перетаскивание Drag элементов ИЗ коллекции Collection View

Реализовать Drag протокол очень просто, и первое, что вы всегда должны делать, это установливать себя, self, в качестве делегата dragDelegate:

Drag & Drop в ваших iOS приложениях - 24

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

Drag & Drop в ваших iOS приложениях - 25

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

Drag & Drop в ваших iOS приложениях - 26

Единственным обязательным методом протокола UICollectionViewDragDelegate является метод itemsForBeginning, который скажет Drag системе, ЧТО мы «перетаскиваем». Метод itemsForBeginning вызывается, когда пользователь начинает «перетаскивать» (Dragging) ячейку коллекции cell.

Заметьте, что в этот метод коллекция Collection View добавила indexPath. Это подскажет нам, какой элемент коллекции, какой indexPath, мы собираемся «перетаскивать». Для нас это действительно очень удобно, так как именно на приложение возлагается ответственность по использованию аргументов session и indexPath для выяснения того, как обращаться с этим «перетаскиванием» Drag.

Если возвращается массив [UIDragItems] «перетягиваемых» элементов, то «перетягивание» Drag инициализируется, если же возвращается пустой массив [ ], то «перетягивание» Drag игнорируется.

Я создам небольшую private функцию dragItems (at: indexPath) с аргументом indexPath. Она возвращает нужный нам массив [UIDragItem].

Drag & Drop в ваших iOS приложениях - 27

На что похож «перетаскиваемый» элемент UIDragItem?
У него есть только одна очень ВАЖНАЯ вещь, которая называется itemProvider. itemProvider — это просто нечто, что может обеспечить данными то, что будет перетаскиваться.

И вы вправе спросить: “А как быть с «перетаскиванием» элемента UIDragItem, у которого просто нет данных?” У элемента, который вы хотите перетаскивать, может не быть данных, например, по причине того, что создание этих данных является затратной операцией. Это может быть изображение image или что-то требующее загрузки данных из интернета. Замечательно то, что операция Drag & Drop является полностью асинхронной. Когда вы начинаете «перетаскивание» Drag, то это реально очень легковесный объект (lift preview), вы таскаете его повсюду, и ничего не происходит во время этого «перетаскивания». Но как только вы “бросаете” Drop куда-то свой объект, то он, являясь itemProvider, действительно должен снабдить ваш «перетаскиваемый» и “брошенный” объект реальными данными, даже если это потребует определенного времени.

К счастью, есть множество встроенных itemProviders. Это классы, которые уже существуют в iOS и которые являются itemPoviders, такие, например, как NSString, который позволяет перетаскивать текст без шрифтов. Конечно, это изображение UIImage. Вы можете выбрать и перетаскивать повсюду изображения UIImages. Класс NSURL, что совершенно замечательно. Вы можете зайти на Web страницу, выбрать URL и “бросить” его куда хотите. Это может быть ссылка на статью или URL для изображения, как это будет в нашем в демонстрационном примере. Это классы цвета UIColor, элемента карты MKMapItem, контакта CNContact из адресной книги, множество вещей вы можете выбирать и «перетаскивать». Все они являются itemProviders.

Мы собираемся «перетаскивать» изображение UIImage. Оно находится в ячейке коллекции Collection View с indexPath, который помогает мне выбрать ячейку cell, достать из нее Outlet imageView и получить его изображение image.

Давайте выразим эту идею парой строк кода.
Сначала я запрашиваю мою коллекцию Collection View о ячейки cell для элемента item, соответствующего этому indexPath.

Drag & Drop в ваших iOS приложениях - 28

Метод cellForItem (at: IndexPath) для коллекции Collection View работает только для видимых (visible) ячеек, но, конечно, он будет работать в нашем случае, ведь я «перетаскиваю» Drag элемент коллекции, находящийся на экране, и он является видимым.

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

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

Drag & Drop в ваших iOS приложениях - 29

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

Drag & Drop в ваших iOS приложениях - 30

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

Но прежде чем я верну dragItem, я собираюсь сделать еще одну вещь, а именно, установить переменную localObject для dragItem, равную полученному изображению image.

Drag & Drop в ваших iOS приложениях - 31

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

Написанный нами код будет работать при «перетаскивании» Drag за пределы нашей коллекции Collection View в другие приложения, но если мы «перетаскиваем» Drag локально, то мы можем использовать localObject. Далее я возвращаю массив, состоящий из одного элемента dragItem.

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

Drag & Drop в ваших iOS приложениях - 32

Кроме локального объекта localObject, можно запомнить локальный контекст localContext для нашей Drag сессии session. В нашем случае это будет коллекция collectionView и она пригодится нам позже:

Drag & Drop в ваших iOS приложениях - 33

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

Drag & Drop в ваших iOS приложениях - 34

Возврат пустого массива [ ] из метода itemsForAddingTo приводит к тому, что жест tap будет интерпретироваться обычным образом, то есть как выбор этой ячейки cell.
И это все, что нам необходимо для «перетаскивания» Drag.
Запускаем приложение.
Я выбираю изображение “Венеция”, держу его некоторое время и начинаю двигать…

Drag & Drop в ваших iOS приложениях - 35

… и мы действительно можем перетащить это изображение в приложение Photos, так как вы видите зеленый плюсик "+" в левом верхнем углу «перетаскиваемого» изображения. Я могу выполнить жест tap еще на одном изображении «Артика» из коллекции Collection View

Drag & Drop в ваших iOS приложениях - 36

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

Drag & Drop в ваших iOS приложениях - 37

Так как в приложение Photos уже встроен механизм Drag & Drop, то все работает прекрасно, и это круто.
Итак, у меня работает «перетягивание» Drag и «сброс» Drop изображения Галереи в другие приложения, мне не пришлось многое делать в моем приложении, за исключением поставки изображения image как массива [UIDragItem]. Это одно из многих замечательных возможностей механизма Drag & Drop — очень легко заставить его работать в обоих направлениях.

Сброс Drop изображений В коллекцию Collection View

Теперь нам нужно сделать Drop часть для моей коллекции Collection View, чтобы можно было «сбрасывать» Drop любые «перетаскиваемые» изображения ВНУТРЬ этой коллекции. «Перетаскиваемое» изображение может «приходить» как ИЗВНЕ, так и непосредственно ИЗНУТРИ этой коллекции.
Для этого мы делаем то же самое, что делали с делегатом dragDelegate, то есть делаем себя, self, делегатом dropDelegate в методе viewDidLoad:

Drag & Drop в ваших iOS приложениях - 38

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

Drag & Drop в ваших iOS приложениях - 39

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

Drag & Drop в ваших iOS приложениях - 40

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

Drag & Drop в ваших iOS приложениях - 41

Если мы реализуем эти два метода, то мы можем получить маленький зелененький плюсик "+”, когда будем перетаскивать изображения ИЗВНЕ на нашу коллекцию Сollection View, а кроме того, нам не будут пытаться сбрасывать то, чего мы не понимаем.

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

Drag & Drop в ваших iOS приложениях - 42

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

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

Drag & Drop в ваших iOS приложениях - 43

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

Drag & Drop в ваших iOS приложениях - 44

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

Drag & Drop в ваших iOS приложениях - 45

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

Метод canHandle сообщает о том, что мы можем обрабатывать только такого рода «перетаскивания» Drag на нашу коллекцию Collection View. В противном случае дальше вообще не имеет смысла вести разговор о «сбросе» Drop.

Если мы продолжаем «сброс» Drop, то еще до того момента, как пользователь поднимет пальцы от экрана и произойдет реальный «сброс» Drop, мы должны сообщить iOS с помощью метода dropSessionDidUpdate делегата UICollectionViewDropDelegateо нашем предложениии UIDropProposal по выполнению сброса Drop.

В этом методе мы должны вернуть Drop предложение, которое может иметь значения .copy или .move или .cancel или .forbiddenдля аргумента operation. И это все возможности, которыми мы располагаем в обычном случае, когда имеем дело с обычным UIView.

Но коллекция Collection View идет дальше и предлагает вернуть специализированное предложениии UICollectionViewDropProposal, которое является subclass класса UIDropProposal и позволяет помимо операции operation указать также дополнительный параметр intent для коллекции Collection View.

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

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

Drag & Drop в ваших iOS приложениях - 46

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

Drag & Drop в ваших iOS приложениях - 47 

Я опять использовала вычисляемую константа isSelf, и если это self реорганизация, то я выполняю перемещение .move, в противном случае я выполняю копирование .copy. В обоих случаях мы используем .insertAtDestinationIndexPath, то есть вставку новых ячеек cells.

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

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

Drag & Drop в ваших iOS приложениях - 48

Я могу кликнуть еще на паре изображений в Safari, и «перетаскиваемых» изображений станет уже 3:

Drag & Drop в ваших iOS приложениях - 49

Но если я подниму палец и «сброшу» Drop эти изображения, то они не разместятся в нашей Галерее, а просто вернутся на прежние места, потому что мы еще не реализовали метод performDrop.

Drag & Drop в ваших iOS приложениях - 50

Вы могли видеть, что коллекция Collection View уже знает, что я хочу делать.
Коллекция Collection View — совершенно замечательная вещь для механизма Drag & Drop, у нее очень мощный функционал для этого. Мы едва прикоснулись к ней, написав 4 строчки кода, а она уже достаточно далеко продвинулась в восприятии “сброса” Drop.
Давайте вернемся в код и реализуем метод performDrop.

Drag & Drop в ваших iOS приложениях - 51

В этом методе нам не удастся обойтись 4-мя строчками кода, потому что метод performDrop немного сложнее, но не слишком.
Когда происходит “сброс” Drop, то в методе performDrop мы должны обновить нашу Модель, которой является Галерея изображений imageGallery со списком изображений images, и мы должны обновить нашу визуальную коллекцию collectionView.

У нас возможны два различных сценария “сброса” Drop.

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

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

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

Drag & Drop в ваших iOS приложениях - 52

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

Drag & Drop в ваших iOS приложениях - 53

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

Теперь мы знаем, где мы будем производить “сброс” Drop. Мы должны пройти по всем «сбрасываемым» элементам coordinator.items, предоставляемым координатором coordinator. Каждый элемент item из этого списка имеет ТИП UICollectionViewDropItem и может предоставить нам очень интересные куски информации.

Например, если я смогу получить sourceIndexPath из item.sourceIndexPath, то я точно буду знать, что это «перетаскивание» Drag выполняется от самого себя, self, и источником перетаскивания Drag является элемент коллекции с indexPath равным sourceIndexPath:

Drag & Drop в ваших iOS приложениях - 54

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

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

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

Drag & Drop в ваших iOS приложениях - 55

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

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

Drag & Drop в ваших iOS приложениях - 56

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

Drag & Drop в ваших iOS приложениях - 57

Я удалила элемент коллекции collectionView с sourceIndexPath и вставила новый элемент коллекции с destinationIndexPath.

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

Но есть реально крутой способ обойти это, который состоит в том, что у коллекции collectionView есть метод с именем performBatchUpdates, который имеет замыкание (closure) и внутри этого замыкания я могу разместить любое число этих deleteItems, insertItems, moveItems и все, что я хочу:

Drag & Drop в ваших iOS приложениях - 58

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

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

Drag & Drop в ваших iOS приложениях - 59

Как только вы поднимаете палец от экрана, изображение перемещается, все происходит в одно и то же время: “сброс”, исчезновение изображения в одном месте и появление в другом.
Попробуем переместить тестовое изображение «Венеция» в нашей Галерее изображений в конец первой строк…

Drag & Drop в ваших iOS приложениях - 60

… и «сбросить» его:

Drag & Drop в ваших iOS приложениях - 61

Как мы и хотели, оно разместилось в конце первой строки.
Ура! Все работает!

Теперь займемся НЕ локальным случаем, то есть когда «сбрасываемый» элемент приходит ИЗВНЕ, то есть из другого приложения.
Для этого в коде мы пишем else по отношению к sourceIndexPath. Если у нас нет sourceIndexPath, то это означает, что «сбрасываемый» элемент пришел откуда-то ИЗВНЕ и нам придется задействовать передачу данных с использованием itemProver сбрасываемого" элемента item.dragItem.itemProvider:

Drag & Drop в ваших iOS приложениях - 62

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

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

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

Я буду использовать инициализатор для контекста местозаменителя  placeholderContext, который “бросает” dragItem на  UICollectionViewDropPlaceholder:

Drag & Drop в ваших iOS приложениях - 63

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

Drag & Drop в ваших iOS приложениях - 64

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

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

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

Drag & Drop в ваших iOS приложениях - 65

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

Drag & Drop в ваших iOS приложениях - 66

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

Drag & Drop в ваших iOS приложениях - 67

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

Drag & Drop в ваших iOS приложениях - 68

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

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

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

Drag & Drop в ваших iOS приложениях - 69

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

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

Drag & Drop в ваших iOS приложениях - 70

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

Drag & Drop в ваших iOS приложениях - 71

Если обе локальные переменные imageURL и aspectRatio не равны nil, мы попросим контекст местозаменителя placeholderСontext с помощью метода commitInsertion дать нам возможность изменить нашу Модель imageGallery

Drag & Drop в ваших iOS приложениях - 72

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

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

ВСЕГДА используйте здесь insertionIndexPath, и ТОЛЬКО insertionIndexPath, для обновления вашей Модели.

Как мы обновляем нашу Модель?

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

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

Drag & Drop в ваших iOS приложениях - 73

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

Drag & Drop в ваших iOS приложениях - 74

Необходимо иметь ввиду одну особенность URLs, которые приходят из мест наподобие Google, в действительности они нуждаются в незначительных преобразованиях для получения “чистого” URL для изображения. Как решается эта проблема можно увидеть в этом демонстрационном приложении в файле Utilities.swift на Github.
Поэтому при получении URL изображения мы используем свойство imageURL из класса URL:

Drag & Drop в ваших iOS приложениях - 75

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

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

Drag & Drop в ваших iOS приложениях - 76

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

Drag & Drop в ваших iOS приложениях - 77

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

Drag & Drop в ваших iOS приложениях - 78

Мы можем продолжить «сброс» изображений и разместить в нашей коллекции еще больше изображений:

Drag & Drop в ваших iOS приложениях - 79

После «сброса» работают Placeholder:

Drag & Drop в ваших iOS приложениях - 80

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

Drag & Drop в ваших iOS приложениях - 81

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

Drag & Drop в ваших iOS приложениях - 82

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

Drag & Drop в ваших iOS приложениях - 83

Конечно, мы можем добавить кэш для изображений imageCache:

Drag & Drop в ваших iOS приложениях - 84

Мы будем наполнять imageCache при «сбросе» Drop в методе performDrop

Drag & Drop в ваших iOS приложениях - 85

и при выборке из «сети» в пользовательском классе ImageCollectionViewCell:

Drag & Drop в ваших iOS приложениях - 86

А использовать кэш imageCache будем при воспроизведении ячейки cell нашей Галереи изображений в пользовательском классе ImageCollectionViewCell:

Drag & Drop в ваших iOS приложениях - 87

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

Drag & Drop в ваших iOS приложениях - 88

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

Drag & Drop в ваших iOS приложениях - 89

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

Drag & Drop в ваших iOS приложениях - 90

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

Drag & Drop в ваших iOS приложениях - 91

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

Drag & Drop в ваших iOS приложениях - 92

Присходит загрузка изображений и Placeholders работает…

Drag & Drop в ваших iOS приложениях - 93

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

Drag & Drop в ваших iOS приложениях - 94

Итак, мы многое умеем делать с нашей Галереей изображений: наполнять ее ИЗВНЕ, реорганизовывать элементы ВНУТРИ, делиться изображениями с другими приложениями.
Нам осталось научить ее избавляться от ненужных изображений путем «сброса» их Drop в «мусорный бак», представленный на навигационной панели справа. Как описано в разделе «Возможности демонстрационного приложения „Галерея изображений“» «мусорный бак» представлен классом GabageView, который наследует от UIView и мы должны научить его принимать изображения из нашей коллекции Сollection View.

Сброс Drop изображений Галереи в «мусорный бак».

Сразу с места — в карьер. Я добавлю к GabageView “взаимодействие” interaction и это будет UIDropInteraction, так как я пытаюсь получить «сброс» Drop какой-то вещи. Все, чем мы должны обеспечить этот UIDropInteraction, это делегат delegate, и я собираюсь назначить себя, self, этим делегатом delegate:

Drag & Drop в ваших iOS приложениях - 95

Естественно, наш класс GabageView должен подтвердить, что мы реализует протокол UIDropInteractionDelegate:

Drag & Drop в ваших iOS приложениях - 96

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

Drag & Drop в ваших iOS приложениях - 97

Однако в отличие от аналогичных методов для коллекции Collection View, у нас нет никакой дополнительной информации в виде indexPath места сброса.

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

Drag & Drop в ваших iOS приложениях - 98

В методе canHandle по существу вы просто сообщаете, что если «перетаскиваемый» объект не является изображением UIImage, то дальше не имеет смысла продолжать «сброс» Drop и вызывать последующие методы.
Если же «перетаскиваемый» объект является изображением UIImage, то мы будем выполнять метод sessionDidUpdate. Все, что нам нужно сделать в этом методе, это вернуть наше предложение UIDropProposal по «сбросу» Drop. И я готова принять только «перетаскиваемый» ЛОКАЛЬНО объект ТИПА изображения UIImage, который может быть «сброшен» Drop где угодно внутри моего GarbageView. Мой GarbageView не будет взаимодействовать с изображениями, сброшенными ИЗВНЕ. Поэтому я анализирую с помощью переменной session.localDragSession, имеет ли место локальный «сброс» Drop, и возвращаю предложение «сброса» в виде конструктора UIDropProposal с аргументом operation, принимающим значение .copy, потому что ВСЕГДА ЛОКАЛЬНОЕ «перетаскивание» Drag в моем приложении будет происходить из коллекции Collection View. Если происходит «перетаскивание» Drag и «сброс» Drop ИЗВНЕ, то я возвращаю предложение «сброса» в виде конструктора UIDropProposal с аргументом operation, принимающим значение .fobbiden, то есть «запрещено» и мы вместо зеленого плюсика "+" получим знак запрещения «сброса».

Drag & Drop в ваших iOS приложениях - 99

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

Drag & Drop в ваших iOS приложениях - 100

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

Drag & Drop в ваших iOS приложениях - 101

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

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

Drag & Drop в ваших iOS приложениях - 102

С помощью локальной Drag сессии  localDragSession нашей Drop сессии session нам удалось получить все «перетягиваемые» на GarbageView Drag элементы items, а их может быть много, как мы знаем, и все они являются изображениями нашей колллекции collectionView. Создавая Drag элементы dragItems нашей коллекции Collection View, мы предусмотрели для каждого «перетягиваемого» Drag элемента dragItem локальный объект localObject, который является изображением image, однако оно нам не пригодилось при внутренней реорганизации коллекции collectionView, но при «сбросе» изображений Галереи в «мусорный бак» мы остро нуждаемся в локальном объекте localObject «перетягиваемого» объекта dragItem, ведь на этот раз у нас нет координатора coordinator, который так щедро делится информацией о том, что происходит в коллекции collectionView. Поэтому мы хотим, чтобы локальным объектом localObject был индекс indexPath в массиве изображений images нашей Модели imageGallery. Внесем необходимые изменения в метод dragItems(at indexPath: IndexPath) класса ImageGalleryCollectionViewController:

Drag & Drop в ваших iOS приложениях - 103

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

Drag & Drop в ваших iOS приложениях - 104

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

Drag & Drop в ваших iOS приложениях - 105

Запускаем приложение, наполняем Галерею новыми изображениями:

Drag & Drop в ваших iOS приложениях - 106

Выделяем пару изображений, которые хотим удалить из нашей Галерее…

Drag & Drop в ваших iOS приложениях - 107

… «бросаем» их на иконку с «мусорным баком»…

Drag & Drop в ваших iOS приложениях - 108

Они уменьшаются практически до 0…

Drag & Drop в ваших iOS приложениях - 109

… и исчезают из коллекции Collection View, скрывшись в «мусорном баке»:

Drag & Drop в ваших iOS приложениях - 110

Сохранение изображений между запусками.

Для сохранения Галереи изображений между запусками мы будем использовать UserDefaults, предварительно преобразовав нашу Модель в JSON формат. Для этого мы добавим в наш Controller переменную var defailts

Drag & Drop в ваших iOS приложениях - 111

..., а в структуры Модели ImageGallery и ImageModel протокол Codable:

Drag & Drop в ваших iOS приложениях - 112

Строки String, массивы Array, URL и Double уже реализуют протокол Codable, поэтому нам больше ничего не придется делать, чтобы заставить работать кодировку и декодировку для Mодели ImageGallery в JSON формат.
Как нам получить JSON версию ImageGallery?
Для этого создаем вычисляемую переменную var json, которая возвращает результат попытки преобразования себя, self, с помощью JSONEncoder.encode() в JSON формат:

Drag & Drop в ваших iOS приложениях - 113

И это все. Будут возвращаться либо данные Data как результат преобразования self в формат JSON, либо nil, если не удастся выполнить это преобразование, хотя последнее никогда не происходит, потому что этот ТИП 100% Encodable. Использована Optional переменная json просто из соображений симметрии.
Теперь у нас есть способ преобразования Модели ImageGallery в Data формата JSON. При этом переменная json имеет ТИП Data?, который можно запоминать в UserDefaults.
Теперь представим, что каким-то образом нам удалось получить JSON данные json, и я хотела бы воссоздать из них нашу Модель, экземпляр структуры ImageGallery. Для этого очень легко написать ИНИЦИАЛИЗАТОР для ImageGallery, входным аргументом которого являются JSON данные json. Этот инициализатор будет “падающим” инициализатором (failable). Если он не сможет провести инициализацию, то он “падает” и возвращает nil:

Drag & Drop в ваших iOS приложениях - 114

Я просто получаю новое значение newValue с помощью декодера JSONDecoder, пытаясь раскодировать данные json, которые передаются в мой инициализатор, а затем присваиваю его self.
Если мне удалось это сделать, то я получаю новый экземпляр ImageGallery, но если моя попытка заканчивается неудачей, то я возвращаю nil, так как моя инициализация “провалилась”.
Надо сказать, что здесь у нас намного больше причин “провалиться” (fail), потому что вполне возможно, что JSON данные json могут быть испорчены или пусты, все это может привести к “падению” (fail) инициализатора.

Теперь мы можем реализовать ЧТЕНИЕ JSON данных и восстановление Модели imageGallery в методе viewWillAppear нашего Controller

Drag & Drop в ваших iOS приложениях - 115

… а также ЗАПИСЬ в наблюдателе didSet{} свойства imageGallery:

Drag & Drop в ваших iOS приложениях - 116

Давайте запустим приложение и наполним нашу Галерею изображениями:

Drag & Drop в ваших iOS приложениях - 117

Если мы закроем приложение и откроем его вновь, то увидим нашу предыдущую Галерею изображений, которая сохранилась в UserDefaults.

Заключение.

В этой статье на примере очень простого демонстрационного приложения «Галерея изображений» продемонстрировано, как легко можно внедрить технологию Drag & Drop в iOS приложение. Это позволило полноценно редактировать Галерею Изображений, «забрасывая» туда новые изображения из других приложений, перемещая существующие и удаляя ненужные. А также раздавать накопленные в Галерее изображения в другие приложения.

Конечно, нам бы хотелось создавать множество таких тематических живописных коллекций изображений и сохранять их непосредственно на iPad или на iCloud Drive. Это можно сделать, если интерпретировать каждую такую Галерею как постоянно хранимый документ UIDocument. Такая интерпретация позволит нам подняться на следующий уровень абстракции и создать приложение, работающее с документами. В таком приложении ваши документы будет показывать компонент DocumentBrowserViewController, очень похожий на приложение Files. Он позволит вам создавать документы UIDocument типа «Галерея изображений» как на вашем iPad, так и на iCloud Drive, а также выбирать нужный документ для просмотра и редактирования.
Но это уже предмет следующей статьи.

P.S. Код демонстрационного приложения до внедрения механизма Drag & Drop и после находится на Github.

 

Автор: WildGreyPlus

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js