Site icon AppTractor

Встречайте: новый протокол Transferable

На 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 есть три ипостаси:

Так как наша структура проста и легко подписываема под 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-объекты. В замыкании мы указываем то, каким образом будет обработана их передача:

Также от нас ожидается возврат либо true (чтобы разрешить передачу), либо false (чтобы запретить).

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

Exit mobile version