- Drag & Drop в ваших iOS приложениях
- Lift (подъем)
- Drag (перетаскивание)
- Drop (сбрасывание)
- Data Transfer (передача данных)
- Возможности демонстрационного приложения «Галерея изображений»
- Перетаскивание Drag элементов ИЗ коллекции Collection View
- Сброс Drop изображений В коллекцию Collection View
- Сброс Drop изображений Галереи в «мусорный бак».
- Сохранение изображений между запусками.
- Заключение.
Drag & Drop в ваших iOS приложениях
Механизм 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 .
Методы 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 , а также выбирать нужный документ для просмотра и редактирования.
Но это уже предмет следующей статьи.
Источник