На WWDC 2022 Apple представила множество интересных нововведений, одно из который — Transferable. О новом протоколе (только для SwiftUI и только для iOS 16, macOS 13 и tvOS 16), который позволяет удобно и быстро передавать какие-либо данные как внутри приложения, так и между приложениями рассказывают разработчики студии CleverPumpkin.
Немного информации про UTType
Перед изучением самого Transferable следует немного освежить в памяти то, как устроена идентификация типов данных на Apple платформах. Уже знающие могут это пропустить.
Так как система хранит все данные в двоичном формате, ей надо как-то идентифицировать типы данных. Потому что по сути нули и единицы никак не говорят о том, какой тип данных они репрезентуют. Как раз для того, чтобы можно было отличать одни нули и единички от других и используется идентификатор типа. Начиная с iOS 14 мы можем использовать очень удобный инструмент для оперирования типами в iOS — UTType. Чтобы начать его использовать, нужно импортировать UniformTypeIdentifiers к себе в проект:
import UniformTypeIdentifiers
UTType предоставляет возможность системе и приложениям идентифицировать тип данных. Например, с помощью него мы можем сохранять несколько разнотипных элементов в буфер обмена:
UIPasteboard.general.items = [ [UTType.text.identifier: "Meet Transferable"], [UTType.image.identifier: swiftUILogo] ]
Идентификатор типа — это строка вида public.<type_name>. Вот несколько примеров системных типов:
UTType.text // public.text UTType.image // public.image UTType.data // public.data
Также, для понимания того, как типы друг с другом соотносятся, Apple имеет систему наследования идентификаторов типов данных. Так, например, UTType.text наследуется от UTType.data, а она в свою очередь наследуется от UTType.item.
Начало работы
Давайте создадим небольшое приложение на SwiftUI, где мы будем в рамках одного приложения передавать данные с помощью Transferable между двумя вьюшками. Одна вьюшка будет кодировать данные и передавать их, а вторая будет их принимать, декодировать и как-то реагировать на полученные данные.
Чтобы начать работать с Transferable, нужно импортировать библиотеку, которая предоставляет API для работы с ним. Эта библиотека содержится в SwiftUI модуле, поэтому если вы импортируете SwiftUI, то этот фреймворк также будет импортирован:
import CoreTransferable
Чтобы создать какую-то структурку, которую мы сможем куда-то передавать, нам надо создать соответствующий ей тип данных. Делается это через UTType. Стоит отметить, что большое количество системных типов уже соответствуют протоколу Trasferable, поэтому для передачи текста, картинок или стандартных цветов, создавать новый тип данных не нужно.
Однако мы сделаем собственный тип данных, который будем передавать между вьюшками. Давайте создадим тип MyColor, с которым мы будем работать по ходу этой статьи. Для этого мы переходим в Targets → Info → Exported Type Identifiers и там объявляем новый тип:
Программируем Transferable
Теперь нам нужно сделать использование этого типа возможным. Для этого следует создать константу, которая содержит данный тип:
extension UTType { static let myColor = UTType(exportedAs: "ru.cleverpumpkin.Meet.mycolor") }
Дальше создадим структуру, которую мы будем разными способами использовать в наших приложениях:
struct MyColor: Codable { let name: String let red: Double let green: Double let blue: Double }
Чтобы уметь передавать данную структуру через новый протокол, надо ее подписать под этот протокол и реализовать единственную переменную, которую требует этот протокол:
extension MyColor: Transferable { static var transferRepresentation: some TransferRepresentation { // Репрезентация } }
Здесь можно видеть новый протокол TransferRepresentation. Этот протокол требует от нас какой-то репрезентации нашей структуры.
У TransferRepresentation есть три ипостаси:
- CodableRepresentation — передача данных, описанных структурой, которая реализует протокол Codable.
- FileRepresenation — передача данных путем сохранения на диск и передача URL. Apple советует использовать этот тип репрезентации для шеринга больших объемов данных.
- DataRepresentation — передача данных, которые могут быть особым образом закодированы в Data и декодированы из Data.
Так как наша структура проста и легко подписываема под Codable, мы будем делать именно эту репрезентацию:
extension MyColor: Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(contentType: .myColor) } }
Да! Все настолько просто. Теперь система может спокойно кодировать и декодировать наши данные в MyColor. Ниже представлены варианты того, как могут выглядеть другие репрезентации для нашей структуры:
extension MyColor: Transferable { static var transferRepresentation: some TransferRepresentation { DataRepresentation(contentType: .myColor) { myColor in myColor.convertToData() } importing: { data in MyColor(data: data) } } } extension MyColor: Transferable { static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .myColor) { myColor in SentTransferredFile(myColor.saveAndReturnURL()) } importing: { receivedTransferredFile in MyColor(url: receivedTransferredFile.file) } } }
Также существует еще один особый вид репрезентации — ProxyRepresentation. Она позволяет использовать уже существующую (другую) репрезентацию как валидную для нашей структуры, например, экспорт нашей структуры в строку может выглядеть так:
extension MyColor: Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(contentType: .myColor) ProxyRepresentation(exporting: \.name) } }
В данном случае и репрезентация через MyColor.self, и через String.self будут правильно работать. Таким образом, мы теперь еще можем экспортировать нашу структуру в строку, и будет передано то, что хранится в переменной name.
Тестовый проект для использования Transferable
Создадим небольшой проект, где мы сделаем возможность перетаскивать цвета с помощью Drag-and-Drop.
Для начала нам надо сделать объект, который мы сможем перемещать (квадратик с цветом):
struct DraggableColor: View { let myColor: MyColor var body: some View { Color(myColor: myColor) .draggable(myColor) .frame(width: 50, height: 50) .cornerRadius(8) } }
Здесь мы добавили новый модификатор .draggable(myColor), который в себя принимает Transferable. Мы туда передали хранящийся в структуре myColor, это значит, что при перетаскивании мы будем передавать наш myColor.
Теперь нужно создать то, куда мы будем вставлять наш цвет. В MyColor у нас содержится цвет и название цвета, поэтому мы создадим вьюху, отображающую цвет и текст с названием цвета:
struct DropRectangle: View { @Binding var draggedMyColor: MyColor? var body: some View { VStack { RoundedRectangle(cornerRadius: 8) .foregroundColor(Color(maybeMyColor: draggedMyColor) ?? .gray.opacity(0.4)) .frame(width: 200, height: 130) .dropDestination(for: MyColor.self) { items, location in withAnimation(.easeInOut(duration: 0.15)) { draggedMyColor = items.first } return true // Allow to drop } if let colorName = draggedMyColor?.name { Text(colorName) } } } }
Здесь мы также добавили новый модификатор:
.dropDestination(for: MyColor.self) { items, location in withAnimation(.easeInOut(duration: 0.15)) { draggedMyColor = items.first } return true // Allow to drop }
Который позволяет принимать draggable-объекты. В замыкании мы указываем то, каким образом будет обработана их передача:
- items — это объекты, которые нам были переданы. Здесь они будут иметь тип MyColor.
- location — это позиция (CGPoint), на которой остановился пользователь.
Также от нас ожидается возврат либо true (чтобы разрешить передачу), либо false (чтобы запретить).
И… Все! Теперь мы можем запускать проект и тестировать. Вот так в пару десятков строк мы сделали довольно сложное действие, которое раньше могло занять в разы больше времени.