Протокол Transferable меняет правила игры для Drag & Drop в SwiftUI

На WWDC 2022 среди других интересных анонсов Apple представила новый протокол в Swift под названием Transferable. Transferable позволяет очень легко и просто копировать данные между разными точками в одном и том же приложении или в разных приложениях. И когда речь идет о копировании, это включает не только Copy & Paste (копирование и вставку), но и Drag & Drop (перетаскивание и «сброс»).

До появления протокола Transferable передача данных как между отдельными частями одного приложения, так и между разными приложениями, осуществлялась классом class NSItemProvider. Именно этот класс делает очень сложные вещи, связанные с передачей данных между процессами. Он также решает проблемы с безопасностью и определенно там есть многопоточность, потому что мы не хотим блокировать UI двух приложений в случае передачи между ними больших по объему  изображений. Класс NSItemProvider управлял всем этим вместо нас. К сожалению, класс NSItemProvider — это до-Swift класс, и мы должны использовать “as” как  “мостик” между такими Swift ТИПами как String, и такими ТИПами старого NS Мира, как NSString, это Objective-C вещи, и “as” является “мостом” в этот старый мир. До появления протокола Transferable технология Drag & Drop (перетаскивание и сброс) была одним из таких мест соприкосновения «старого» и нового Миров.

Новый протокол Transferable можно использовать только в операционных системах iOS 16+, macOS 13+ (Ventura и новее), watchOS 9.0+ и tvOS 16 +. Фактически, протокол Transferable заменил NSItemProvider, и для тех, кто хочет программировать операции Copy & Paste (копирование и вставку) и Drag & Drop (перетаскивание и «сброс») он меняет правила игры.

У протокола Transferable есть только одно требование: указать хотя бы одно представления передаваемых данных в static свойстве transferRepresentation:Многие Swift ТИПы уже реализуют протокол Transferable:

  • String
  • Data
  • URL
  • Attributed String
  • Image

Для ваших оригинальных Моделей Данных Apple предоставила API различных представлений передаваемых данных TransferRepresentation, но вы также можете определить и свои собственные представления. Определяемые системой представления передаваемых данных TransferRepresentation охватывают большинство случаев использования. Они включают:

  • CodableRepresentation — Передача данных, описанных структурой, которая реализует протокол Codable
  • DataRepresentation — Передача данных, которые могут быть особым образом закодированы в Data и декодированы из Data.
  • FIleRepresentation — Передача данных путем сохранения информации на диске и передача URL. Apple советует использовать этот тип представления для больших объемов данных.
  • ProxyRepresentation — для передачи альтернативного контента того же самого представления.

В результате протоколу Transferable для Copy & Paste (копирования и вставки) или для Drag & Drop (перетаскивание и «сброса») потребуется всего несколько строк кода для выполнения всей тяжелой работы «за кулисами» для:

  • пользовательских ТИПов, реализующих протокол Codable.
  • данных Data
  • файлов Files

В этом посте мы рассмотрим, как перетаскивать (Drag and Drop) с помощью протокола Transferable данные пользовательского ТИПа, которые реализуют протокол Codable. И лучше всего это показано в статье «Опыт использования протокола Transferable для Drag & Drop в SwiftUI» (First Experience With Transferable Implementing Drag And Drop In SwiftUI), перевод который я и сделаю.

Следующий пост будет посвящен передаче альтернативного контента данные пользовательского Codable ТИПа с помощью  ProxyRepresentation, а также  рассмотрим применение DataRepresentation и FileRepresentation. Основой для этого поста послужила статья «Протокол Transferable в SwiftUI — передача альтернативного контента с помощью ProxyRepresentation» (Transferable Protocol in SwiftUI – Transferring Alternative Content With ProxyRepresentation).

В третьем посте мы рассмотрим на демонстрационном примере EmojiArt из Стэнфордских курсов «Разработка iOS 14 приложений «с помощью SwiftUI» случай, когда на одну область «сброса» могут «сбрасываться» различные ТИПы данных.

Демонстрационный проект

Чтобы понять, что необходимо для перетаскивания (Drag & Drop) с помощью протокола Transferable, мы сосредоточимся на реализации небольшого проекта, в котором будем отображать список цветовых элементов, а также небольшой View, который послужит местом “сброса” для любого из этих цветовых элементов. Ничего особенного, но достаточно, чтобы говорить о протоколе Transferable.

Самое главное, что каждый отображаемый цвет будет программно представлен пользовательским ТИПом, который реализует протокол Codable. Это имеет особое значение, поскольку половину работы, связанной с протоколом Transferable, будет выполняться с помощью  протокола Codable.

Приложение, которое мы будем здесь создавать, представлено в самом начале этой статьи. Уточнение: цвета взяты на Colors.

Подготовка демонстрационного приложения

Несмотря на то, что в конце этого поста есть ссылка для скачивания демонстрационного проекта, мы быстро создадим его с нуля. Как только мы это сделаем, можно будет сосредоточиться исключительно на обсуждении Transferable.

Итак, в новом проекте на основе SwiftUI, который мы назовем TransferableDemo (обязательно используйте Xcode 14 в качестве минимально необходимой версии), добавьте новый SwiftUI View файл. Вы можете назвать его ColorView.swift, так как он будет отображать цветовой элемент. Основной файл ContentView.swift, созданный по умолчанию, пока не изменяйте.

У этого простейшего View имеется ZStack, в котором отображается цвет Color, заданный объектом colorItem, a поверх него название этого цвета colorItem.name. Несколько модификаторов стилизуют находящиеся внутри ZStack цвет Color и текст Text.

Что касается ТИПа ColorItem, то мы определим его несколько позже, но, как следует из названия, это программное представление цветового элемента .

Примечание. Если вы создаете это приложения с нуля, следуя описанным здесь шагам, вам необходимо заменить структуру ColorView_Previews на следующую:

Имея в распоряжении ColorView, давайте перейдем к файлу ContentView.swift, который существует в проекте по умолчанию. Добавьте туда следующий код:

Вот вкратце что делает этот код::

  • Он представляет список нескольких цветовых элементов в левой части экрана, отображая каждый такой элемент с помощью ColorView, который мы спроектировали чуть ранее.
  • Для визуального разделения экрана на две части используется разделитель Divider.
  • В правой части экрана находится VStack, который “работает целью” перетаскивания и «сброса». Он отображает либо ColorView со «сброшенным» элементом, либо подсказку в виде Text, если цветовой элемент не перетаскивается и не «сброшен». Обратите внимание, что граница этого VStack получает свои значения (цвет borderColor и ширину границы borderWidth) из соответствующих @State свойств; мы будем обновлять их динамически при “нависании” цветового элемента над этим View.

ТИП Colors в этом фрагменте кода является еще одним пользовательским ТИПом, который мы реализуем чуть позже. Фактически, он является View Model, которая предоставляет нашему View данные, необходимые для отображения.

Тип ColorItem

Два Views почти готовы; единственное, чего еще не хватает, это всего, что связано с протоколом Transferable. Поэтому сосредоточимся на ТИПе ColorItem; мы уже использовали этот ТИП в качестве Модели, но она еще не существует. Уже упоминалось, что ColorItem программно описывает цветовой элемент, отображаемый на экране. Учитывая, что в конечном итоге мы хотим перетаскивать объекты этого ТИПа, необходимо убедиться, что он реализует протокол Codable, поэтому давайте начнем с этого:

Примечание. Вы можете создать новый Swift файл ColorItem.swift с исходным кодом, чтобы разместить там представленный здесь код.

Помимо протокола Codable, ColorItem также реализует протокол Identifiable, предоставляя свойство id, которое используется контейнером ForEach в SwiftUI. У ТИПа ColorItem будут следующие свойства:

Здесь нет ничего сложного; у нас есть свойство id, требуемое протоколом Identifiable, имя цвета name и еще три свойства для хранения значений компонентов красного red, зеленого green и синего blue нашего цвета.

Для облегчения работы с цветовыми элементами, давайте определим несколько образцов цветовых элементов в static свойстве sampleColors, и на этом первый этап работы над ColorItem можно считать законченным:

Наконец, прямо перед тем, как мы начнем работать над Transferable, давайте определим еще один пользовательский ТИП, который мы уже использовали ранее; ТИП Colors:

Определение пользовательского content type UTI

Когда дело доходит до использования Transferable, то для того, чтобы сделать возможным копирование (Copy) или перетаскивание (Drag & Drop) объекта пользовательского ТИПа, нам нужно сделать два специальных шага. 

Первый шаг — сообщить системе, какой ТИП объекта мы хотим передать, или, другими словами, указать унифицированный идентификатор типа Uniform Type Identifier (UTI), также известный как content type.

UTI или content type — это способ, изобретенный Apple для универсального описания различных ТИПов данных, таких как двоичные файлы, изображения, тексты, аудио, видео и многое другое, не говоря уже о таких представлениях данных, как расширения файлов (file extensions), ТИПы MIME, и другие методы, которые потенциально могут привести к двусмысленности или несовместимости. Многие content type являются подтипами других типов; например, content type формата изображений PNG является подтипом content type изображения Image, который, в свою очередь, является подтипом content type двоичных данных Data.

Apple предоставляет большое количество встроенных типов content type, но мы можем определять и свои собственные типы UTI. И это то, что мы обязательно должны сделать в таких ситуациях, как наша. Нам необходимо сообщить системе, какого типа данные мы планируем передавать (transfer).

Обсуждение content type можно продолжать, но это выведет нас за рамки этой статьи. После краткого обзора content type, нам нужно объявить новый content type, который будет представлять наш объект ColorItem.

Для этого кликните на имени проекта в Навигаторе проектов (Project Navigator) и выберите target TransferableDemo (или любое другое имя, которым вы назвали ваше приложение). Затем откройте вкладку Info. Разверните секцию Exported Type Identifiers (“Идентификаторы экспортируемых типов”), кликните на кнопке “+” и заполните следующие поля:

  • Description (описание): ColorItem
  • Identifier (идентификатор): это должно быть уникальное значение, и рекомендуется ставить перед любым значением Bundle Identifier вашего приложения. Например, я установил в своем проекте такой идентификатор com.gabrieltheodoropoulos.TransferableDemo.color.
  • Confirm To (cоответствует): com.public.data

Остальные поля оставьте пустыми, так как они здесь не представляют никакого интереса.

Затем вернитесь к файлу, в котором определяется структура struct ColorItem и добавьте следующее расширение extension:

Примечание. Убедитесь, что вы установили в расширении extension ТИПа UTType в точности то значение, которое вы использовали в поле Identifier (идентификатор) на предыдущем шаге.

ТИП UTType программно представляет UTI, и мы выполняем его расширение extension UTType, чтобы объявить static свойство color, соответствующее пользовательскому content type ColorItem, определенному ранее во вкладке Info нашего проекта. Это не обязательная часть процесса, но предоставляет нам большое удобство; мы будем обращаться только к static свойству color вместо того, чтобы писать полный UTI всякий раз, когда нам нужно его использовать.Обратите внимание, что необходимо импортировать дополнительный фреймворк, чтобы ТИП UTType стал доступным:

Как ColorItem реализует протокол Transferable

После завершения первого шага и определения нового пользовательского content type, следующим шагом мы должны сделать ТИП ColorItem способным передавать (transferring) его экземпляры. Вот что на самом деле происходит “за кулисами”. Любой экземпляр ТИПа ColorItem, который мы собираемся копировать (Copy) или перетаскивать (Drag) с помощью протокола Transferable, сначала сериализуется и копируется в “память”. Как только мы его вставили (Paste) или “бросили” (Drop), он десериализуется после считывания из “памяти”, становится исходным экземпляр и мы можем снова использовать его во всех операциях (Paste или Drop), как исходный.

По умолчанию пользовательские ТИПы, реализующие протокол Codable, автоматически сериализуются в объекты JSON, но можно сериализовать и с использованием других представлений, обеспечивая пользовательским кодером encoder и декодером decoder объекта. Однако этим мы сейчас заниматься не будем.

Опять возвращаемся к нашей структуре struct ColorItem и заставляем ее реализовать протокол Transferable следующим образом:

Прежде чем идти дальше, убедитесь, что вы импортировали import фреймворк SwiftUI, который предоставляет API для Transferable:

У протокола Transferable только одно требование: указать хотя бы один передаваемый (transfer) элемент в static свойстве transferRepresentation:

Здесь мы можем использовать определенные API. В нашем конкретном случае, когда мы имеем дело с Codable ТИПом, мы используем CodableRepresentation, как показано ниже:

Первый аргумент в инициализаторе CodableRepresentation можно опустить, однако он оставлен, чтобы продемонстрировать, как можно явно указать передаваемый (transferable) ТИП. В данном случае это ColorItem.self.

Однако аргумент contentType нельзя опускать; это обязательный аргумент, и именно здесь мы устанавливаем content type (UTI) пользовательского передаваемого ТИПа. Обратите внимание, что вместо того, чтобы писать полный UTI путем инициализации экземпляра класса UTType, мы просто обращаемся к static свойству color, которое мы ранее объявили в расширении extension UTType, используя dot синтаксис, что, несомненно, очень удобно.

Есть еще два необязательных аргумента, которые мы также можем передать вышеуказанному инициализатору CodableRepresentation. Это пользовательские encoder и decoder на случай, если мы хотим сериализовать с использованием представления, отличного от JSON.

Это все, что нам нужно для того, чтобы можно было копировать-вставлять (Copy-Paste) или перетаскивать и “бросать” (Drag & Drop) объекты пользовательского ТИПа ColorItem. Протокол Transferable управляет всем этим автоматически “за кулисами”, не требуя никаких дополнительных усилий с нашей стороны.

Возможность “перетаскивания” (dragging)

Теперь, когда ТИП ColorItem успешно реализует протокол Transferable, пришло время дать возможность Views осуществлять перетаскивание и “сброс” (Drag & Drop), чтобы мы могли перемещать цветовые элементы ColorItem в нашем приложении. Таким View является ColorView.

Чтобы “включить” режим перетаскивания, необходимо добавить к самому внешнему View определенный View модификатор; это draggable(_:):

Аргумент colorItem, который мы предоставляем этому модификатору, — это объект, который мы хотим перетаскивать. Излишне говорить очевидное, но это должен быть объект, имеющий ТИП, реализующий протокол Transferable. После наших последних действий с ТИПом ColorItem так оно и есть.

Прием “сброса” (dropping)

View для “сброса” перетаскиваемого цветового элемента, — это VStack в ContentView. Как мы видели в начале статьи, это View отображает либо перетаскиваемый цветового элемент draggedColorItem с помощью ColorView, либо текстовое сообщение Text с подсказкой:

Чтобы позволить перетаскиваемому цветовому элементу попасть в этот VStack, мы должны использовать другой View модификатор — dropDestination. Вот он в самом развернутом виде:

Первый аргумент этого модификатора, указывающий ТИП перетаскиваемого элемента ColorItem.self, является необязательным, поэтому его можно опустить. Аргумент isTargeted, представляющий собой замыкание, также необязателен, но далее у нас будет возможность поговорить об этом. Однако второй аргумент, который также является первым замыканием, является обязательным.

Он имеет два параметра:

  • items — это массив, содержащий все перетаскиваемые элементы, но, очевидно, что при перетаскивании одного элемента этот массив будет содержать только один элемент.
  • location сообщает о координатах сброса” в наш VStack перетаскиваемого элемента, при этом левый верхний угол является нулевой точкой (0, 0). Иногда это значение может оказаться полезным.

Давайте заполним недостающие части. В первом замыкании в массиве items есть только один элемент. Мы назначим его @State свойству draggedColorItem, чтобы оно отображалось в VStack. Однако мы не будем делать ничего особенного в отношении места “сброса” location; мы просто печатаем координаты места “сброса” в момент прохождения перетаскиваемого элемента.

В первом замыкании помимо некоторых действий, нужно вернуть из него Bool значение: true, если перетаскиваемый элемент можно “сбросить”, в противном случае — значение false. Не все сценарии всегда будут такими простыми как наш. Иногда вам понадобиться фильтровать перетаскиваемые элементы items, а иногда вам нужно будет отказаться от их “сброса”.

Давайте добавим код для тех действий, которые я только что описал:

Наконец, есть второе замыкание, которое все еще пусто. Оно интересно нам только в том случае, если вы хотите знать, находится ли перетаскиваемый элемент draggedColorItem внутри или вне области “сброса” (drop area), и если вы хотите выполнить какие-либо визуальные изменения или другие действия, зависящие от этого.

Параметром второго замыкания является Bool значение, которое устанавливается равным true, если перетаскиваемый элемент (dragged item) находится внутри границ области “сброса” (drop area). В нашем случае мы обновляем цвет borderColor и ширину borderWidth границы VStack следующим образом:

Каждый раз, когда перетаскиваемый элемент (dragged item) находится внутри области “сброса” (drop area), граница области “сброса” будет отображаться определенным цветом и линией увеличенной ширины, a при выходе из этой области “сброса” (drop area) граница будет возвращаться к нормальному состоянию, которое было до входа в область “сброса” (drop area).

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

Заключение

Хотя статья получилась немного длинноватой из-за необходимости объяснять некоторые дополнительные понятия, а также подготовить демонстрационное приложение, мы установили, что реализация протокола Transferable не потребовала от нас больших усилий. Не будет преувеличением сказать, что протокол Transferable в корне меняет правила игры, когда мы копируем (Copy-Paste) или перетаскиваем и “бросаем” (Drag & Drop) данные, поскольку он позволяет нам делать это с минимальными возможными усилиями. То, что представлено в этой статье, является лишь частью Transferable; помимо ТИПов Codable, можно передавать альтернативный контент Codable ТИПов с помощью  ProxyRepresentation, а также осуществлять передачу двоичных данных с помощью DataRepresentation и файлов с помощью FileRepresentation.

Вы можете скачать код представленного в статье проекта по ссылке.

Следующий пост о Transferable.

Источники:

Transferable Protocol in SwiftUI – Transferring Alternative Content With ProxyRepresentation

First Experience With Transferable Implementing Drag And Drop In SwiftUI

The Transferable Protocol