Механизм 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 (перетаскивание)
Drag (перетаскивание) — это когда пользователь перемещает объект по поверхности экрана. В процессе этой фазы «предварительный просмотр» (lift preview
) для этого объекта может модифицироваться (появляется зеленый плюсик "+" или другой знак)…
… разрешено также некоторое взаимодействие с системой: можно кликнуть на каком-то другом объекте и добавить его к текущей сессии «перетаскивания»:
Drop (сбрасывание)
Drop (сбрасывание) происходит, когда пользователь поднимает палец. В этот момент могут произойти две вещи: либо Drag
объект будет уничтожен, либо произойдет «сброс» Drop
объекта в месте назначения.
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
:
Центральной частью приложения безусловно является 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:
Public API
класса ImageCollectionViewCell — это URL
изображения imageURL. Как только мы его устанавливаем, наш UI
обновляется, то есть асинхронно выбираются данные для изображения по этому imageURL и отображаются в ячейке. Пока идет выборка данных из сети, работает индикатор активности spinner, показывающий, что мы в процессе выборки данных.
Я использую для получения данных по заданному URL
глобальную очередь global (qos: .userInitiated) с аргументом «качества обслуживания» qos, который установлен в .userInitiated, потому что я выбираю данные по просьбе пользователя:
Каждый раз, когда вы используете внутри замыкания собственные переменные, в нашем случае это imageView и imageURL, компилятор заставляет вас ставить перед ними self., чтобы вы спросили себя: «А не возникает ли здесь “циклическая ссылка памяти” (memory cycle
)?» У нас нет здесь явной “циклической ссылки памяти” (memory cycle
), потому что у самого self нет указателя на это замыкание.
Тем не менее, в случае многопоточности вы должны принять во внимание, что ячейки cells в коллекции Collection View
являются повторно-используемыми благодаря методу dequeueReusableCell. Каждый раз, когда ячейка (новая или повторно-используемая) попадает на экран, запускается асинхронно загрузка изображения из сети (в это время крутится «колесико» индикатора активности spinner).
Как только загрузка выполнена и изображение получено, происходит обновление UI
этой ячейки коллекции. Но мы не ждем загрузки изображения, мы продолжаем прокручивать коллекцию и примеченная нами ячейка коллекции уходит с экрана, так и не обновив свой UI
. Однако снизу должно появиться новое изображение и эта же ячейка, ушедшая с экрана, будет использована повторно, но уже для другого изображения, которое, возможно, быстро загрузится и обновит UI
. В это время вернется ранее запущенная в этой ячейки загрузка изображения и обновит экран, что приведет к неправильному результату. Это происходит потому, что мы запускаем разные вещи, работающие с сетью, в разных потоках. Они возвращаются в разное время.
Как мы можем исправить ситуацию?
В пределах используемого нами механизма GCD мы не можем отменить загрузку изображения ушедшей с экрана ячейки, но мы можем, когда приходят из сети наши данные imageData, проверить URL
url, который вызвал загрузку этих данных, и сравнить его с тем, который пользователь хочет иметь в этой ячейки в данный момент, то есть imageURL. Если они не совпадают, то мы не будем обновлять UI
ячейку и подождем нужных нам данных изображения:
Эта абсурдная на первый взгляд строка кода url == self.imageURL заставляет все работать правильно в многопоточной среде, которая требует нестандартного воображения. Дело в том, что некоторые вещи в многопоточном программировании происходят в другом порядке, чем написан код.
Если выборку данных изображения не удалось выполнить, то формируется изображение с сообщением об ошибке в виде строки «Error» и эмоджи с «нахмуренное лицо». Просто пустое пространство в нашей коллекции Collection View
может немного запутать пользователя:
Нам бы не хотелось, чтобы изображение с сообщением об ошибке повторяло aspectRatio этого ошибочного изображения, потому что в этом случае текст вместе с эмоджи будет растягиваться или сжиматься. Нам бы хотел, чтобы оно было нейтральным — квадратным, то есть имело бы соотношение сторон aspectRatio близкое к 1.0.
Мы должны сообщить об этом пожелании нашему Controller
, чтобы он исправил в своей Модели imageGallery соотношение сторон aspectRatio для соответствующего indexPath. Это интересная задача, есть много путей ее решения, и мы выберем наиболее легкий из них — использование Optional замыкания (closure
) var changeAspectRatio: (() -> Void)?. Оно может равняться 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
в наше приложение состоит в следующем:
- cначала мы наделим нашу коллекцию изображений
Collection View
способностью «перетягивать»Drag
ИЗ нее изображения UIImage как вовне, так и локально, - затем мы научим нашу коллекцию изображений
Collection View
принимать «перетянутые»Drag
извне или локально изображения UIImage, - мы также научим наше 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:
И, конечно, в самом верху класса ImageGalleryCollectionViewController вы должны сказать, что “Да”, мы реализуем протокол UICollectionViewDragDelegate:
Как только мы это сделаем, компилятор начинает “жаловаться”, мы кликаем на красном кружочке и нас спрашивают: “Хотите добавить обязательные методы протокола UICollectionViewDragDelegate?”
Я отвечаю: “Конечно, хочу!” и кликаю на кнопке Fix
:
Единственным обязательным методом протокола UICollectionViewDragDelegate является метод itemsForBeginning, который скажет Drag
системе, ЧТО мы «перетаскиваем». Метод itemsForBeginning вызывается, когда пользователь начинает «перетаскивать» (Dragging
) ячейку коллекции cell.
Заметьте, что в этот метод коллекция Collection View
добавила indexPath. Это подскажет нам, какой элемент коллекции, какой indexPath, мы собираемся «перетаскивать». Для нас это действительно очень удобно, так как именно на приложение возлагается ответственность по использованию аргументов session и indexPath для выяснения того, как обращаться с этим «перетаскиванием» Drag
.
Если возвращается массив [UIDragItems] «перетягиваемых» элементов, то «перетягивание» Drag
инициализируется, если же возвращается пустой массив [ ], то «перетягивание» Drag
игнорируется.
Я создам небольшую private функцию dragItems (at: indexPath) с аргументом indexPath. Она возвращает нужный нам массив [UIDragItem].
На что похож «перетаскиваемый» элемент 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.
Метод cellForItem (at: IndexPath) для коллекции Collection View
работает только для видимых (visible
) ячеек, но, конечно, он будет работать в нашем случае, ведь я «перетаскиваю» Drag
элемент коллекции, находящийся на экране, и он является видимым.
Итак, я получила «перетаскиваемую» ячейку cell.
Далее я применяю оператор as? к этой ячейке, чтобы она имела ТИП моего пользовательского subclass
. И если это работает, то я получаю Outlet
imageView, у которого беру его изображение image. Я просто “захватила” изображение image для этого indexPath.
Теперь, когда у меня есть изображение image, все, что мне необходимо сделать, это создать один из этих UIDragItems, используя полученное изображение image в качестве itemProvider, то есть вещи, которая обеспечивает нас данными.
Я могу создать dragItem с помощью конструктора UIDragItem, который берет в качестве аргумента itemProvider:
Затем мы создаем itemProvider для изображения image также с помощью конструктора NSItemProvider. Существует несколько конструкторов для NSItemProvider, но среди них есть один действительно замечательный — NSItemProvider (object:NSItemProviderWriting):
Этому конструктору NSItemProvider вы просто даете объект object, и он знает, как сделать из него itemProvider. В качестве такого объекта object я даю изображение изображение image, которое я получила из ячейки cell и получаю itemProvider для UIImage.
И это все. Мы создали dragItem и должны вернуть его как массив, имеющий один элемент.
Но прежде чем я верну dragItem, я собираюсь сделать еще одну вещь, а именно, установить переменную localObject для dragItem, равную полученному изображению image.
Что это означает?
Если вы выполняете «перетаскивание» Drag
локально, то есть внутри вашего приложения, то вам нет необходимости проходить через весь этот код, связанный с itemProvider, через асинхронное получение данных. Вам не нужно ничего этого делать, вам нужно просто взять localObject и использовать его. Это своего рода “короткое замыкание” при локальном «перетаскивании» Drag
.
Написанный нами код будет работать при «перетаскивании» Drag
за пределы нашей коллекции Collection View
в другие приложения, но если мы «перетаскиваем» Drag
локально, то мы можем использовать localObject. Далее я возвращаю массив, состоящий из одного элемента dragItem.
Между прочим, если я не смогла получить по каким-то причинам image для этой ячейки cell, то я возвращаю пустой массив [ ], это означает, что «перетаскивание» Drag
отменяется.
Кроме локального объекта localObject, можно запомнить локальный контекст localContext для нашей Drag
сессии session. В нашем случае это будет коллекция collectionView и она пригодится нам позже:
Начав «перетаскивание» Drag
, вы можете добавлять еще больше элементов items к этому «перетаскиванию», просто выполнив жест tap на них. В результате вы можете перетаскивать Drag
множество элементов за один раз. И это легко реализовать с помощью другого метода делегата UICollectionViewDragDelegate, очень похожего на метод itemsForВeginning, метода с именем itemsForAddingTo. Метод itemsForAddingTo выглядит абсолютно точно также, как метод itemsForВeginning, и возвращает абсолютно ту же самую вещь, потому что он также дает нам indexPath того, на чем “тапнул” пользователь в процессе «перетаскивания» Drag
, и мне достаточно получить изображение image из ячейке, на которой “тапнул” пользователь, и вернуть его.
Возврат пустого массива [ ] из метода itemsForAddingTo приводит к тому, что жест tap будет интерпретироваться обычным образом, то есть как выбор этой ячейки cell.
И это все, что нам необходимо для «перетаскивания» Drag
.
Запускаем приложение.
Я выбираю изображение “Венеция”, держу его некоторое время и начинаю двигать…
… и мы действительно можем перетащить это изображение в приложение Photos
, так как вы видите зеленый плюсик "+" в левом верхнем углу «перетаскиваемого» изображения. Я могу выполнить жест tap еще на одном изображении «Артика» из коллекции Collection View
…
… и теперь уже мы можем бросить два изображения в приложение Photos
:
Так как в приложение Photos
уже встроен механизм Drag & Drop
, то все работает прекрасно, и это круто.
Итак, у меня работает «перетягивание» Drag
и «сброс» Drop
изображения Галереи в другие приложения, мне не пришлось многое делать в моем приложении, за исключением поставки изображения image как массива [UIDragItem]. Это одно из многих замечательных возможностей механизма Drag & Drop
— очень легко заставить его работать в обоих направлениях.
Сброс Drop
изображений В коллекцию Collection View
Теперь нам нужно сделать Drop
часть для моей коллекции Collection View
, чтобы можно было «сбрасывать» Drop
любые «перетаскиваемые» изображения ВНУТРЬ этой коллекции. «Перетаскиваемое» изображение может «приходить» как ИЗВНЕ, так и непосредственно ИЗНУТРИ этой коллекции.
Для этого мы делаем то же самое, что делали с делегатом dragDelegate, то есть делаем себя, self, делегатом dropDelegate в методе viewDidLoad:
Мы опять должны подняться в верхнюю часть нашего класса ImageGalleryCollectionViewController и подтвердить реализацию протокола UICollectionViewDropDelegate:
Как только мы добавили наш новый протокол, компилятор опять начал “жаловаться”, что мы этот протокол не реализовали. Кликаем на кнопке Fix
, и перед нами появляются обязательные методы этого протокола. В данном случае нам сообщают, что мы должны реализовать метод performDrop:
Мы должны это сделать, иначе не произойдет “сброс” Drop
. В действительности я собираюсь реализовать метод performDrop в последнюю очередь, потому что есть пара других настоятельно рекомендуемых Apple
методов, которые необходимо реализовать для Drop
части. Это canHandle и dropSessionDidUpdate:
Если мы реализуем эти два метода, то мы можем получить маленький зелененький плюсик "+”, когда будем перетаскивать изображения ИЗВНЕ на нашу коллекцию Сollection View
, а кроме того, нам не будут пытаться сбрасывать то, чего мы не понимаем.
Давайте реализуем canHandle. У нас с вами версия метода canHandle, которая предназначается для коллекции Сollection View
. Но именно этот метод Сollection View
выглядит абсолютно точно также, как аналогичный метод для обычного UIView, там нет никакого indexPath. Нам нужно просто вернуть session.canLoadObjects (ofClass:UIImage.self), и это означает, что я принимаю “сброс” объектов этого класса в моей коллекции Сollection View
:
Но этого недостаточно для «сброса» 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):
Мне осталось определить, имею ли я дело со «сбросом» ИЗВНЕ или ВНУТРИ. Я буду это делать с помощью вычисляемой константы isSelf, для вычисления которой я могу использовать такую вещь у Drop
сессии session, как её локальная Drag
сессия localDragSession. У этой локальной Drag
сессии в свою очередь есть локальный контекст localContext.
Если вы помните, мы устанавливали этот локальный контекст в методе itemsForВeginning Drag
делегата UICollectionViewDragDelegate:
Я буду исследовать локальный контекст localContext на равенство моей коллекции collectionView. Правда ТИП у localContext будет Any, и мне необходимо сделать «кастинг» ТИПА Any с помощью оператора as? UICollectionView:
Если локальный контекст (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:
В нашем случае мы всегда хотим добавлять новую ячейку и параметр intent примет значение .insertAtDestinationIndexPath в противоположность .insertIntoDestinationIndexPath.
Я опять использовала вычисляемую константа isSelf, и если это self реорганизация, то я выполняю перемещение .move, в противном случае я выполняю копирование .copy. В обоих случаях мы используем .insertAtDestinationIndexPath, то есть вставку новых ячеек cells.
Пока я не реализовала метод performDrop, но давайте взглянем на то, что уже может делать коллекция Collection View
с этой маленькой порцией информации, которую мы ей предоставили.
Я «перетаскиваю» изображение из Safari
с поисковой системой Google
, и у этого изображения появляется сверху зеленый знак "+", сообщающий о том, что наша Галерия Изображений готова не только принять и скопировать это изображение вместе с его URL
, но и предоставить место внутри коллекции Collection View
:
Я могу кликнуть еще на паре изображений в Safari
, и «перетаскиваемых» изображений станет уже 3:
Но если я подниму палец и «сброшу» Drop
эти изображения, то они не разместятся в нашей Галерее, а просто вернутся на прежние места, потому что мы еще не реализовали метод performDrop.
Вы могли видеть, что коллекция Collection View
уже знает, что я хочу делать.
Коллекция Collection View
— совершенно замечательная вещь для механизма Drag & Drop
, у нее очень мощный функционал для этого. Мы едва прикоснулись к ней, написав 4 строчки кода, а она уже достаточно далеко продвинулась в восприятии “сброса” Drop
.
Давайте вернемся в код и реализуем метод performDrop.
В этом методе нам не удастся обойтись 4-мя строчками кода, потому что метод performDrop немного сложнее, но не слишком.
Когда происходит “сброс” Drop
, то в методе performDrop мы должны обновить нашу Модель, которой является Галерея изображений imageGallery со списком изображений images, и мы должны обновить нашу визуальную коллекцию collectionView.
У нас возможны два различных сценария “сброса” Drop
.
Есть “сброс” Drop
осуществляется из моей коллекции collectionView, то я должна выполнить “сброс” Drop
элемента коллекции на новом месте и и убрать его со старого места, потому что в этом случае я перемещаю (.move) этот элемент коллекции. Это тривиальная задача.
Есть “сброс” Drop
осуществляется из другого приложения, то мы должны использовать свойство itemProvider «перетаскиваемого» элемента item для выборки данных.
Когда мы выполняем “сброс” Drop
в коллекции collectionView, то коллекция предоставляет нам координатор coordinator. Первое и наиболее важное, что нам сообщает координатор coordinator, это destinationIndexPath, то есть indexPath “пункта-назначения” “сброса” Drop
, то есть куда мы будем “сбрасывать”.
Но destinationIndexPath может быть равен nil, так как вы можете перетащить «сбрасываемое» изображение в ту часть коллекции Collection View
, которая не является местом между какими-то уже существующими ячейками cells, так что он вполне может равняться nil. Если происходит именно эта ситуация, то я создаю IndexPath с 0-м элементом item в 0 -ой секции section.
Я могла бы выбрать любой другой indexPath, но этот indexPath я буду использовать по умолчанию.
Теперь мы знаем, где мы будем производить “сброс” Drop
. Мы должны пройти по всем «сбрасываемым» элементам coordinator.items, предоставляемым координатором coordinator. Каждый элемент item из этого списка имеет ТИП UICollectionViewDropItem и может предоставить нам очень интересные куски информации.
Например, если я смогу получить sourceIndexPath из item.sourceIndexPath, то я точно буду знать, что это «перетаскивание» Drag
выполняется от самого себя, self, и источником перетаскивания Drag
является элемент коллекции с indexPath равным sourceIndexPath:
Мне даже не надо смотреть на localСontext в этом случае, чтобы узнать, что это «перетаскивание» было сделано ВНУТРИ коллекции collectionView. Здорово!
Теперь я знаю источник sourceIndexPath и “пункт-назначения” destinationIndexPath Drag & Drop
, и задача становится тривиальной. Все, что мне необходимо сделать, это обновить Модель так, чтобы источник и “пункт-назначения” поменялись местами, а затем обновить коллекцию collectionView, в которой нужно будет убрать элемент коллекции с sourceIndexPath и добавить его в коллекцию с destinationIndexPath.
Наш локальный случай — самый простейший, потому что в этом случае механизм Drag & Drop
работает не просто в том же самом приложении, но и в той же самой коллекции collectionView, и я могу получать всю необходимую информацию с помощью координатора coordinator. Давайте его реализуем этот простейший локальный случай:
В нашем случае мне не понадобится даже localObject, который я “припрятала” ранее, когда создавала dragItem и который я могу заимствовать теперь у «перетаскиваемого» элемента коллекции item в виде item.localObject. Он нам понадобится при «сбросе» Drop
изображений в «мусорный бак», который находится в том же самом приложении, но не является той же самой коллекцией collectionView. Сейчас мне достаточно двух IndexPathes: источника sourceIndexPath и “пункта-назначения” destinationIndexPath.
Сначала я получаю информацию imageInfo об изображении на старом месте из Модели, убирая его оттуда. А затем вставляю в массив images моей Модели imageGallery информацию imageInfo об изображении с новым индексом destinationIndexPath.item. Вот так я обновила мою Модель:
Теперь я должна обновить саму коллекцию collectionView. Очень важно понимать, что я не хочу перегружать все данные в моей коллекции collectionView с помощью reloadData() в середине процесса «перетаскивания» Drag
, потому что это переустанавливает целый “Мир” нашей Галереи изображений, что очень плохо, НЕ ДЕЛАЙТЕ ЭТОГО. Вместо этого я собираюсь убирать и вставлять элементы items по отдельности:
Я удалила элемент коллекции collectionView с sourceIndexPath и вставила новый элемент коллекции с destinationIndexPath.
Выглядит так, как будто бы этот код прекрасно работает, но в действительности, этот код может “обрушить” ваше приложение. Причина заключается в том, что вы делаете многочисленные изменения в вашей коллекции collectionView, а в этом случае каждый шаг изменения коллекции нужно нормально синхронизировать с Моделью, что в нашем случае не соблюдается, так как мы выполняем обе операции одновременно: удаление и вставку. Следовательно, коллекция collectionView будет находиться в какой-то момент в НЕ синхронизированном состоянии с Моделью.
Но есть реально крутой способ обойти это, который состоит в том, что у коллекции collectionView есть метод с именем performBatchUpdates, который имеет замыкание (closure
) и внутри этого замыкания я могу разместить любое число этих deleteItems, insertItems, moveItems и все, что я хочу:
Теперь deleteItems и insertItems будут выполняться как одна операция, и никогда не будет наблюдаться отсутствие синхронизации вашей Модели с коллекцией collectionView.
И, наконец, последняя вещь, которую нам необходимо сделать, это попросить координатор coordinator осуществить и анимировать сам “сброс” Drop
:
Как только вы поднимаете палец от экрана, изображение перемещается, все происходит в одно и то же время: “сброс”, исчезновение изображения в одном месте и появление в другом.
Попробуем переместить тестовое изображение «Венеция» в нашей Галерее изображений в конец первой строк…
… и «сбросить» его:
Как мы и хотели, оно разместилось в конце первой строки.
Ура! Все работает!
Теперь займемся НЕ локальным случаем, то есть когда «сбрасываемый» элемент приходит ИЗВНЕ, то есть из другого приложения.
Для этого в коде мы пишем else по отношению к sourceIndexPath. Если у нас нет sourceIndexPath, то это означает, что «сбрасываемый» элемент пришел откуда-то ИЗВНЕ и нам придется задействовать передачу данных с использованием itemProver сбрасываемого" элемента item.dragItem.itemProvider:
Если вы что-то “перетаскиваете” 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:
Объект, который я собираюсь “бросить” Drop
, это item.dragItem, где item — это элемент for цикла, так как мы можем “бросать” Drop
множество объектов coordinator.items. Мы “бросаем” их один за другим. Итак, item.dragItem — это то, что мы «перетаскиваем» Drag
и «бросаем» Drop
. Следующим аргументом этой функции является местозаменитель, и я создам его с помощью инициализатора UICollectionViewDropPlaceholder:
Для того, чтобы сделать это, мне нужно знать, ГДЕ я собираюсь вставлять местозаменитель 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
. Это сразу же создает нам вторую ячейку, которая является точной копией первой.
Выделяем нашу новую ячейку ImageCell
, устанавливаем идентификатор “DropPlaceholderCell
”, удаляем оттуда все UI
элементы, включая Image View
, так как этот ПРОТОТИП используется тогда, когда изображение еще не поступило. Добавляем туда из Палитры Объектов новый индикатор активности Activity Indicator
, он будет вращаться, давая понять пользователям, что я ожидаю некоторых “сброшенных” данных. Изменим также цвет фона Background
, чтобы понимать, что при «сбросе» изображений ИЗВНЕ работает именно эта ячейка cell как ПРОТОТИП:
Кроме того ТИП новой ячейки не должен быть ImageCollectionVewCell, потому что в ней не будет изображений. Я сделаю эту ячейку обычной ячейкой ТИПА UIСollectionCiewCell, так как нам не нужны никакие Outlets
для управления:
Давайте сконфигурируем индикатор активности Activity Indicator
таким образом, чтобы он начал анимировать с самого начала, и мне не пришлось бы ничего писать в коде, чтобы запустить его. Для этого нужно кликнуть на опции Animating
:
И это все. Итак, мы сделали все установки для этой ячейки DropPlaceholderCell
, возвращаемся в наш код. Теперь у нас есть прекрасный местозаменитель Placeholder
, готовый к работе.
Все, что нам осталось сделать, это получить данные, и когда данные будут получены, мы просто скажем об этом контексту placeholderСontext и он поменяет местами местозаменитель Placeholder
и нашу «родную» ячейку с данными, а мы сделаем изменения в Модели.
Я собираюсь “загрузить” ОДИН объект, которым будет мой item с помощью метода loadObject(ofClass: UIImage.self)(единственное число). Я использую код item.dragItem.itemProvider с поставщиком itemProvider, который обеспечит меня данными элемента item АСИНХРОННО. Ясно, что если подключился iitemProvider, то объект “сброса” iitem мы получаем за пределами данного приложения. Далее следует метод loadObject (ofСlass: UIImage.self) (в единственном числе):
Это конкретное замыкание выполняется НЕ на main queue
. И, к сожалению, нам пришлось переключиться на main queue
с помощью DispatchQueue.main.async {} для того, чтобы «поймать» соотношение сторон изображения в локальную переменную aspectRatio.
Мы действительно ввели две локальные переменные imageURL и aspectRatio …
… и будем «ловить» их при загрузки изображения image и URL url:
Если обе локальные переменные imageURL и aspectRatio не равны nil, мы попросим контекст местозаменителя placeholderСontext с помощью метода commitInsertion дать нам возможность изменить нашу Модель imageGallery:
В этом выражении у нас есть 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..
Если мы по некоторым причинам не смогли получить соотношение сторон изображения aspectRatio и URL
изображения imageURL из соответствующего provider, возможно, была получена ошибка error вместо provider, то мы должны дать знать контексту placeholderContext, что нужно уничтожить этот местозаменитель Placeholder
, потому что мы все равно мы не сможем получить других данных:
Необходимо иметь ввиду одну особенность URLs
, которые приходят из мест наподобие Google
, в действительности они нуждаются в незначительных преобразованиях для получения “чистого” URL
для изображения. Как решается эта проблема можно увидеть в этом демонстрационном приложении в файле Utilities.swift
на Github.
Поэтому при получении URL
изображения мы используем свойство imageURL из класса URL:
И это все, что нужно сделать, чтобы принять ИЗВНЕ что-то внутрь коллекции Collection View
.
Давайте посмотрим это в действии. Запускаем одновременно в многозадачном режиме наше демонстрационное приложение ImageGallery
и Safari
с поисковой системой Google
. В Google
мы ищем изображения на тему «Рассвет» (sunrise). В Safari
уже встроен Drag & Drop
механизм, поэтому мы можем выделить одно из этих изображений, долго удерживать его, немного сдвинуть и перетащить в нашу Галерею Изображений.
Наличие зеленого плюсика "+" говорит о том, что наше приложение готово принять стороннее изображение и скопировать его в свою коллекцию на указанное пользователем место. После того, как мы «сбросим» его, требуется некоторое время на загрузку изображения, и в это время работает Placeholder
:
После завершения загрузки, «сброшенное» изображение размещается на нужном месте, а Placeholder
исчезает:
Мы можем продолжить «сброс» изображений и разместить в нашей коллекции еще больше изображений:
После «сброса» работают Placeholder
:
В результате наша Галерея изображений наполняется новыми изображениями:
Теперь, когда ясно, что мы способны принимать изображения ИЗВНЕ, нам больше не нужны тестовые изображения и мы их убираем:
Наш viewDidLoad становится очень простым: в нем мы делаем наш Controller
Drag
и Drop
делегатом и добавляем распознаватель жеста pinch, который регулирует число изображений на строке:
Конечно, мы можем добавить кэш для изображений imageCache:
Мы будем наполнять imageCache при «сбросе» Drop
в методе performDrop…
и при выборке из «сети» в пользовательском классе ImageCollectionViewCell:
А использовать кэш imageCache будем при воспроизведении ячейки cell нашей Галереи изображений в пользовательском классе ImageCollectionViewCell:
Теперь мы стартуем с пустой коллекции…
… затем «бросаем» новое изображение на нашу коллекцию…
… присходит загрузка изображения и Placeholder
работает…
… и изображение появляется на нужном месте:
Мы продолжаем наполнять нашу коллекцию ИЗВНЕ:
Присходит загрузка изображений и Placeholders
работает…
И изображения появляются на нужном месте:
Итак, мы многое умеем делать с нашей Галереей изображений: наполнять ее ИЗВНЕ, реорганизовывать элементы ВНУТРИ, делиться изображениями с другими приложениями.
Нам осталось научить ее избавляться от ненужных изображений путем «сброса» их Drop
в «мусорный бак», представленный на навигационной панели справа. Как описано в разделе «Возможности демонстрационного приложения „Галерея изображений“» «мусорный бак» представлен классом 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):
В методе 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, то есть «запрещено» и мы вместо зеленого плюсика "+" получим знак запрещения «сброса».
Копируя изображение UIImage, мы будем имитировать уменьшение его масштаба практически до 0, а когда «сброс» произойдет, мы удалим это изображение из коллекции Collection View
.
Для того, чтобы создать у пользователя иллюзию «сброса и исчезновения» изображений в «мусорном баке», мы используем новый для нас метод previewForDropping, который позволяет перенаправить «сброс» Drop
в другое место и при этом трансформировать «сбрасываемый» объект в процессе анимации:
В этом методе c помощью инициализатора UIDragPreviewTarget мы получим новый preView для сбрасываемого объекта target и перенаправим его с помощью метода retargetedPreview на новое место, на «мусорный бак», с уменьшением его масштаба практически до нуля:
Если пользователь поднял палец вверх, то происходит «сброс» 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
сессии 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:
Теперь мы сможем брать у каждого «претаскиваемого» элемента item его localObject, которым является индекс indexPath в массиве изображений images нашей Модели imageGallery, и отправлять его в массив индексов indexes и в массив indexPahes удаляемых изображений:
Зная массив индексов indexes и массив indexPahes удаляемых изображений, в методе performBatchUpdates коллекции collection мы убираем все удаляемые изображения из Модели images и из коллекции collection:
Запускаем приложение, наполняем Галерею новыми изображениями:
Выделяем пару изображений, которые хотим удалить из нашей Галерее…
… «бросаем» их на иконку с «мусорным баком»…
Они уменьшаются практически до 0…
… и исчезают из коллекции Collection View
, скрывшись в «мусорном баке»:
Сохранение изображений между запусками.
Для сохранения Галереи изображений между запусками мы будем использовать UserDefaults, предварительно преобразовав нашу Модель в JSON
формат. Для этого мы добавим в наш Controller
переменную var defailts…
..., а в структуры Модели 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. Использована Optional переменная json просто из соображений симметрии.
Теперь у нас есть способ преобразования Модели ImageGallery в Data формата JSON
. При этом переменная json имеет ТИП Data?, который можно запоминать в UserDefaults.
Теперь представим, что каким-то образом нам удалось получить JSON
данные json, и я хотела бы воссоздать из них нашу Модель, экземпляр структуры ImageGallery. Для этого очень легко написать ИНИЦИАЛИЗАТОР для ImageGallery, входным аргументом которого являются JSON
данные json. Этот инициализатор будет “падающим” инициализатором (failable
). Если он не сможет провести инициализацию, то он “падает” и возвращает nil:
Я просто получаю новое значение newValue с помощью декодера JSONDecoder, пытаясь раскодировать данные json, которые передаются в мой инициализатор, а затем присваиваю его self.
Если мне удалось это сделать, то я получаю новый экземпляр ImageGallery, но если моя попытка заканчивается неудачей, то я возвращаю nil, так как моя инициализация “провалилась”.
Надо сказать, что здесь у нас намного больше причин “провалиться” (fail
), потому что вполне возможно, что JSON
данные json могут быть испорчены или пусты, все это может привести к “падению” (fail
) инициализатора.
Теперь мы можем реализовать ЧТЕНИЕ JSON
данных и восстановление Модели imageGallery в методе viewWillAppear нашего Controller
…
… а также ЗАПИСЬ в наблюдателе didSet{} свойства imageGallery:
Давайте запустим приложение и наполним нашу Галерею изображениями:
Если мы закроем приложение и откроем его вновь, то увидим нашу предыдущую Галерею изображений, которая сохранилась в UserDefaults.
Заключение.
В этой статье на примере очень простого демонстрационного приложения «Галерея изображений» продемонстрировано, как легко можно внедрить технологию Drag & Drop
в iOS
приложение. Это позволило полноценно редактировать Галерею Изображений, «забрасывая» туда новые изображения из других приложений, перемещая существующие и удаляя ненужные. А также раздавать накопленные в Галерее изображения в другие приложения.
Конечно, нам бы хотелось создавать множество таких тематических живописных коллекций изображений и сохранять их непосредственно на iPad или на iCloud Drive. Это можно сделать, если интерпретировать каждую такую Галерею как постоянно хранимый документ UIDocument. Такая интерпретация позволит нам подняться на следующий уровень абстракции и создать приложение, работающее с документами. В таком приложении ваши документы будет показывать компонент DocumentBrowserViewController, очень похожий на приложение Files
. Он позволит вам создавать документы UIDocument типа «Галерея изображений» как на вашем iPad
, так и на iCloud Drive
, а также выбирать нужный документ для просмотра и редактирования.
Но это уже предмет следующей статьи.
P.S. Код демонстрационного приложения до внедрения механизма Drag & Drop
и после находится на Github.
Автор: WildGreyPlus