Протокол Transferable в SwiftUI — передача альтернативного контента с помощью ProxyRepresentation

В предыдущем посте у нас был первый практический опыт работы с протоколом Transferable; новым API, который был представлен на WWDC 2022 и который призван значительно сократить усилия, необходимые с нашей стороны для копирования и вставки (Copy & Paste), a также для перетаскивание и “сброса” (Drag & Drop) данных внутри одного приложения или между разными приложениями.

Если говорить более подробно, то в первом посте продемонстрировано, как перетаскивать и “сбрасывать” (Drag & Drop) объекты пользовательских ТИПов, которые реализуют протокол Codable, при этом было рассмотрено несколько интересных концепций:

  • типы контента (content types UTI) и как декларировать пользовательские UTIs,
  • правильная реализация протокола Transferable, чтобы объекты представленного пользовательского типа UTI можно было перетаскивать,
  • как запустить операцию перетаскивания (Drag) в SwiftUI,
  • как управлять “сбросом” (Drop) перетаскиваемого объекта в SwiftUI.

Этот пост в значительной степени является продолжением предыдущего, поскольку он фокусируется на другой функции протокола Transferable: a именно, как указать дополнительный контент content type для передачи поверх основного перетаскиваемого контента content type.

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

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

В этом посте мы собираемся расширить функциональные возможности первоначальное демонстрационное приложение, добавив текстовое поле TextField в качестве дополнительной области “сброса” (drop area). В конечном итоге мы сделаем возможным перетаскивание цветового элемента в TextField и “сбрасывание” названия цветового элемента name; которое является просто строкой String, а не полным цветовым объектом ColorItem.

Вот демонстрация приложения, которое мы создадим в этом посте:

ProxyRepresentation

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

Это то место, где можно указать, каким будет передаваемый (transferable) объект. В нашем конкретном случае, который демонстрируется в этом и предыдущем постах, у нас есть пользовательский ТИП, который программно описывает цветовой элемент и реализует протокол Codable. Следовательно, нам нужно явно указать, что объекты этого типа будут передаваемыми (transferable) объектами. Вот как мы сделали это в прошлом посте для пользовательского типа ColorItem:

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

Благодаря этому цветовой элемент целиком можно перетаскивать из одного места в другое, при этом фактический экземпляр должен быть сериализован в начале перетаскивания (Drag) и десериализован при”сбросе” (Drop).

Теперь мы собираемся добавить к этому что-то новое. Мы собираемся сделать так, чтобы можно было также передавать исключительно имя name цветового элемента, а не весь объект целиком, или, перефразируя это, мы укажем дополнительное перетаскиваемое представление (transferable representation) исключительно для имени name цветового элемента:

Несколько важных замечаний:

  • Аргумент \.name— это  key path к свойству name, значение которого мы хотим передать (transfer). 
  • ProxyRepresentaton (exporting:) всегда должен вызываться после основного представления (representation). Например, было бы неправильно вызывать ProxyRepresentation (exporting:) перед CodableRepresentation (for:contentType:).
  •  ProxyRepresentation фактически использует основное представление другого ТИПа, как если бы оно было его собственным. Здесь этим другим ТИПом является строка String, потому что свойство name относится к этому ТИПу. Обратите внимание, что в отличие от CodableRepresentation (for:contentType:) здесь не нужно определять content type. Он взят из ТИПа String, который представляет собой обычный текст.

Единственная строка кода  — все, что нам нужно, чтобы заставить ТИП ColorItem передавать (transfer) также и имя name цветового элемента. Следующим шагом является добавление текстового поля TextField в SwiftUI View и включение для него возможности принимать “сброс” (dropping), чтобы мы действительно могли перетаскивать и “сбрасывать” туда (Drag & Drop) имя name цветового элемента.

Добавление TextField к View

В следующем фрагменте кода вы видите ContentView из демонстрационного проекта прошлого поста:

Основном контейнером является HStack, поэтому визуальные элементы должны располагаться на экране горизонтально. С левой стороны в контейнере ForEach находится список всех наших цветовых элементов colors.items. С правой стороны находится VStack, который представляет собой место назначения для “сброса” (drop destination) перетаскиваемого цветовых элементов. Этот VStack содержит либо “сброшенный” цветной элемент draggedColorItem, либо текстовое сообщение Text с подсказкой, если ничего еще не было ”сброшено”.

Мы хотим добавить в область “сброса” текстовое поле TextField, поэтому мы собираемся немного изменить вышеприведенное View. Оставив все как есть до разделителя Divider, мы собираемся встроить VStack в другой VStack следующим образом:

Теперь мы можем добавить текстовое поле TextField прямо там, где находится комментарий “Add the TextField here..” ( “Добавьте TextField сюда…”):

Есть несколько View модификаторов, которые определяют размер frame, границу border и отступы padding текстового поля TextField. Обратите внимание, что @Binding значение свойства с именем colorName передается в качестве аргумента в текстовое поле TextField, но это свойство пока еще не существует в нашем коде; мы должны объявить его в структуре struct ContentView, прежде чем идти дальше:>

Подготовка SwiftUI View завершена, теперь давайте добавим в качестве новой области “сброса” наше текстовое поле TextField.

Установка TextField как места “сброса” для имени name 

Для того, чтобы текстовое поле TextField могло принимать “сброс” (Drop) объектов, необходимо использовать определенный View модификатор; с которым мы уже встречались в предыдущем посте и который вы можете увидеть в ContentView, приведенном в начале этого поста. Это View модификатор dropDestination(for:action:isTargeted:).

Давайте пройдемся по всем аргументам dropDestination:

  • Первый аргумент for — это content type передаваемого элемента. Это необязательный аргумент, и мы можем его не указывать.
  • Второй аргумент action— это замыкание с двумя параметрами: массивом items с переданными элементами  и расположение location пальца в iOS или мыши в macOS внутри области ”сброса”. Мы должны вернуть true или false из этого замыкания в зависимости от того, принимаем мы этот “сброс” или нет. Этот аргумент является обязательным.
  • Последний аргумент isTargeted — это еще одно замыкание, которое также является необязательным. Значение его параметра сообщает нам, находится ли перетаскиваемый элемент внутри или вне области ”сброса”; эта информация, часто оказывается весьма полезной.

Ради демонстрационных целей, мы собираемся использовать все аргументы. Кроме того, нам понадобится последний аргумент для того, чтобы очистить предыдущее название цвета, когда другой цветовой элемент попадает в область текстового поля TextField.

В качестве первого аргумента мы указали ТИП String, как ожидаемый для перетаскиваемых в текстовое поле элементов. Далее добавим отсутствующий контент в замыкание isTargeted:

Когда цветовой элемент будет перетаскиваться над текстовым полем TextField, указанная выше во втором замыкании isTargeted переменная inDropArea станет равна true. В таких случаях мы хотим очистить значение свойства colorName, чтобы новое имя цвета name было установлено в первом замыкании. Говоря об этом, давайте добавим последнюю часть головоломки:

Обратите внимание, что переменная colorName получает либо первый из перетаскиваемых элементов items, которые являются именами цветов, либо пустую строку “ ”, если в массиве items нет перетаскиваемых элементов. Возвращаемое значение замыкания также зависит от наличия элементов в массиве items; если в массиве items есть первый элемент, то есть items.first не равен nil, то возвращается true, иначе false. Наконец, в этом конкретном случае нас не волнует местоположение location перетаскиваемого элемента, поэтому мы просто его игнорируем. C помощью всего лишь нескольких строк кода нам удалось сделать TextField местом “сброса” для перетаскиваемых цветовых элементов.

DataRepresentation и FileRepresentation

DataRepresentation и FileRepresentation используются для представления двоичных данных. DataRepresentation следует использовать для небольших объемов данных, которые можно хранить в памяти. Например, изображения Image.

Экспорт с помощью DataRepresentation

Импорт с помощью DataRepresentation

Вы должны выбрать FileRepresentation, если хотите эффективно передавать большие объемы данных. Система будет передавать URL-адреса файлов, которые ваше приложение может lazy (лениво) загружать в память при необходимости.

Экспорт с помощью FileRepresentation

Импорт с помощью FileRepresentation

Заключение

Хотя пост оказался немного длинноват из-за того, что мы объясняли каждый наш шаг, код наш оказался очень кратким. Начав с проекта, созданного в предыдущем посте, который является общим руководстве по применению протокола Transferable, мы поняли, что необходимо для передачи альтернативного контента в дополнение к оригинальному. Мы умеем использовать DataRepresentation и FileRepresentation и для экспорта и импорта двоичных данных.

Загрузить окончательный проект можно по этой ссылке.

Источники:

Transferable Protocol in SwiftUI – Transferring Alternative Content With ProxyRepresentation

First Experience With Transferable Implementing Drag And Drop In SwiftUI

The Transferable Protocol