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 елемента колекції, куди ви скидаєте” 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! і індикатор активності лічильника: 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 у наш додаток полягає в наступному:

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

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

Працездатність механізму Drag & Drop у вашій колекції Collection View забезпечується двома новими делегатами.
Методи першого делегата, dragDelegate, налаштовані на ініціалізацію і користувацьке налаштування «перетаскиваний» Drags.
Методи <ивторого делегата, 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.

Читайте також  Пол Аллен, співзасновник Microsoft, пішов з життя у віці 65 років

Зауважте, що цей метод колекція 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 в інше місце і при цьому трансформувати «скидається» об’єкт у процесі анімації:

У цьому методі за допомогою ініціалізатор 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, тому нам більше нічого не доведеться робити, щоб змусити працювати кодування і декодування для Моделі 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.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

Вам також сподобається...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *