20 декабря 2022

Meet Transferable

На WWDC 2022 Apple представила множество интересных нововведений, одно из который — Transferable. Новый протокол (только для SwiftUI и только для iOS 16, macOS 13 и tvOS 16), который позволяет удобно и быстро передавать какие-либо данные как внутри приложения, так и между приложениями.
How to upload an app to the App Store and not get rejected
Перед изучением самого 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.
Начало работы
Чтобы начать работать с Transferable, нужно импортировать библиотеку, которая предоставляет API для работы с ним. Эта библиотека содержится в SwiftUI модуле, поэтому если вы импортируете SwiftUI, то этот фреймворк также будет импортирован:


import CoreTransferable

Чтобы создать какую-то структурку, которую мы сможем куда-то передавать, нам надо создать соответствующий ей тип данных. Делается это через UTType. Стоит отметить, что большое количество системных типов уже соответствуют протоколу Trasferable, поэтому для передачи текста, картинок или стандартных цветов, создавать новый тип данных не нужно.

Однако мы сделаем собственный тип данных. Давайте создадим тип MyColor, с которым мы будем работать по ходу этой статьи. Для этого мы переходим в Targets → Info → Exported Type Identifiers и там объявляем новый тип:
Теперь нам нужно сделать использование этого типа возможным. Для этого следует создать константу, которая содержит данный тип:


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. Может использоваться для шеринга больших объемов данных.

  • 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 (чтобы запретить).

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