На WWDC 2022 Apple представила множество интересных нововведений, одно из который — Transferable. О новом протоколе (только для SwiftUI и только для iOS 16, macOS 13 и tvOS 16????), который позволяет удобно и быстро передавать какие-либо данные как внутри приложения, так и между приложениями рассказывают разработчики студии CleverPumpkin.

Перед изучением самого Transferable следует немного освежить в памяти то, как устроена идентификация типов данных на Apple платформах. Всех освежающихся просим под кат, остальные могут это пропустить.

Немного информации про UTType

Так как система хранит все данные в двоичном формате, ей надо как-то идентифицировать типы данных. Потому что по сути нули и единицы никак не говорят о том, какой тип данных они репрезентуют. Как раз для того, чтобы можно было отличать одни нули и единички от других и используется идентификатор типа. Начиная с 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, с которым мы будем работать по ходу этой статьи. Для этого мы переходим в TargetsInfoExportedTypeIdentifiers и там объявляем новый тип:

Теперь нам нужно сделать использование этого типа возможным. Для этого следует создать константу, которая содержит данный тип:

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.


Создадим небольшой проект, где мы сделаем возможность перетаскивать цвета с помощью 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 (чтобы запретить).

И… Все! Теперь мы можем запускать проект и тестировать. Вот так в пару десятков строк мы сделали довольно сложное действие, которое раньше могло занять в разы больше времени.

Комментарии (1)


  1. WildGreyPlus
    24.12.2022 11:20

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

    За операции Drag & Drop (перетаскивание и "сброс") отвечал класс class NSItemProvider. Именно этот класс делает очень сложные вещи, связанные с передачей данных между процессами. Он также решает проблемы с безопасностью и определенно там есть многопоточность, потому что мы не хотим блокировать UI двух приложений в случае передачи между ними больших по объему  изображений.

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

    Почти параллельно с вашей статьей появились две статьи об опыте использования протокол Transferable :

    First Experience With Transferable Implementing Drag And Drop In SwiftUI

    Transferable Protocol in SwiftUI – Transferring Alternative Content With ProxyRepresentation

    Единственное, до чего не "дотягивает" протокол Transferable и соответствующий ему View модификатор dropDestination(for:action:isTargeted:), это возможность указания нескольких UTType.

    Раньше, до iOS 16+:

    Теперь, начиная с iOS 16+:

    Но выход есть:

    Используем перечисление enum
    Используем перечисление enum