Хабрахабр

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

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

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

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

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

Lift (подъем)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть