Streaming is available in most browsers,
and in the Developer app.
-
Meet Transferable
Meet Transferable: a model-layer protocol that allows for effortless support for sharing, drag and drop, copy/paste, and other features in your app. We'll explore how you can use the API for common use cases, and take advantage of advanced features to customize the behavior. We'll also share how you can optimize for memory efficiency when dealing with large amounts of data. Whether you're extending your models to share with other applications as strings or images or creating custom declared data types, Transferable can help you facilitate a great experience in your app.
Resources
Related Videos
WWDC22
- Build a productivity app for Apple Watch
- Enhance collaboration experiences with Messages
- Integrate your custom collaboration app with Messages
- What's new in SwiftUI
Tech Talks
-
Download
♪ Mellow instrumental hip-hop music ♪ ♪ Hello and welcome to the session "Meet Transferable." My name is Julia. I am a SwiftUI engineer, and I am excited to introduce Transferable, a declarative way to support drag and drop, copy/paste, and other functionality in your app.
Apart from SwiftUI and developing applications for Mac, I'm also interested in the story of women in computer science.
I think it's important that we know our heroes.
So I decided to create a catalog application where I can view, add, and edit a list of the female inventors', engineers', and scientists' profiles.
I want this application to seamlessly support drag and drop of the scientists' portraits to and from the app, being able to use the clipboard content to paste interesting facts.
And for the first time, my app can support sharing on watchOS! My potential users say that they would love to be able to share a personality profile from their watch.
Also, via SwiftUI, sharing is now available on iOS and Mac, which also got this brand-new design for ShareSheet this year.
Under the hood, enabling all these features require the models that we already have to support being sent over to a receiver inside our app or even in other applications.
The profile structure contains all the information that we have about a single personality.
All the profiles packed in an archive can be shared among friends.
We store fun facts about the personality in strings and even can attach videos.
And there's a great new easy way to make all these model types to be shared.
Meet Transferable! It is a Swift-first declarative way to describe how your models can be serialized and deserialized for sharing and data transfer.
Today, we are going to be talking about what Transferable is and what is going on behind the scenes when we use it; how to conform custom types; and at the end, I'll share some advanced tips and tricks that can help to customize Transferable to do exactly what you need.
Let's start! Say there are two applications running, and the user wants to pass some information from one app to another via copy/paste, ShareSheet, just drag, or use some other app feature.
When you send something between two different apps, there's all this binary data that goes across.
An important part of sending this data is determining what it corresponds to.
It could be text, a video, my favorite female engineer profile, or whole archive.
And there's the UTType that describes what the data is for.
Let's take a closer look at how apps generate this binary data.
All the types that can be shared with other apps, or even within a single application, have to provide two pieces of information: ways to convert them to and from binary data, and the content type that corresponds to the structure of the binary data and tells the receiver what they actually got.
The content type -- otherwise known as uniform type identifiers -- is an Apple-specific technology that describes identifiers for different binary structures as well as abstract concepts.
The identifiers form a tree, and we can also define custom identifiers.
For example, one for the binary structure used by our profiles.
In order to declare a custom identifier, first, add its declaration to the Info.plist file.
It is also a good idea to add a file extension.
If the data is saved on disk, the system will know that your app can open that file.
Secondly, declare it in code.
To learn more about content types, I invite you to watch a video: "Uniform Type Identifiers -- A reintroduction." Personally, I love it, and it gives a clear idea what are uniform type identifiers and how to use them.
Good news is that many standard types already conform to Transferable; for example, string, data, URL, attributed string, image.
You need only a couple of lines of code to paste fun facts to a profile with the new SwiftUI paste button interface, support dragging images from a view, or receiving a dropped image from Finder or other apps.
Using the br and-new ShareLink, we can now implement sharing experience from the watch.
We covered the basics, and now you have an idea how to use Transferable and what it is.
Let's see how to add Transferable conformance to the models in our application.
As I mentioned earlier, there are four model types in our app that are going to be shared.
One of them -- string -- already conforms to Transferable; we don't need to do anything more.
But what about the single profile, ProfilesArchive, and the video that I want to share as well? To conform a type to Transferable, there's only one property to implement: TransferRepresentation.
It describes how the model is going to get transferred.
There are three important representations to be aware of: CodableRepresentation, DataRepresentation, and FileRepresentation.
We'll talk about each of them.
But first, meet our central model, Profile structure.
It has an id, a name, a bio, maybe some fun facts, a portrait, and a video.
It already conforms to Codable.
Because of that, we can include CodableRepresentation in our Transferable conformance.
Codable representation uses an encoder to convert the profile into binary data, and a decoder to convert it back.
By default, it uses JSON, but you can also provide your own encoder/decoder pair.
To learn more about the Codable protocol and how encoders and decoders work, I invite you to watch a WWDC session where this protocol was first introduced: "Data you can trust." Back to our profile.
The only thing Codable requires is knowing the desired content type.
Since this is going to be a custom format, we will use a custom declared uniform type identifier.
After adding the profile content type, we're good to go.
Profile now conforms to Transferable! Now, let's take look at another case: ProfilesArchive.
It already supports converting to CSV data.
I can export the list of the women profiles in CSV files and then share with friends or import it on another computer.
The archive can be converted to and from data, and it means that we can use the DataRepresentation.
If we peek inside, we'll see that DataRepresentation uses the conversion functions to directly create binary representation and to reconstruct the value for the receiver.
This is how easy it is to conform to Transferable using the DataRepresentation.
All it takes is calling the two functions that we already have: the initializer and the converter to CSV.
If a personality profile has a video attached, I want to be able to drag or share it as well.
But videos can be large; I don't want to load it into memory.
This is where FileRepresentation comes in.
And again, if we lift the curtain, we'll see that FileRepresentation passes the provided URL to the receiver and uses it to reconstruct the Transferable item for them.
FileRepresentation allows us to share items backed by a binary representation written to disk: file.
Let's summarize.
If you want to pick just a single representation for a simple use case, first check if the model has the Codable conformance and doesn't have any specific binary format requirements.
Use CodableRepresentation if it is the case.
If not, check if it is stored in memory or on disk.
DataRepresentation works best for the former, and FileRepresetnation for the latter.
Transferable is meant to cover not only simple use cases, but also complex ones.
And most of the time, with just a few lines of code.
See it for yourself! Previously, we have added Transferable conformance to the profile, but let's go further.
When the profile is copied to the pasteboard and pasted into any text field, I want to paste the profile's name.
This means we need to add another representation.
ProxyRepresentation allows other Transferable types to represent our model.
One line, and Profile can be pasted as text.
Notice that I added the ProxyRepresentation after Codable; the order is important.
The receiver will use the first representation with the content type they support.
If the receiver is aware of our custom content type Profile, they should use it.
If not, but they support text, let them use the ProxyRepresentation instead.
Now, Profile supports both encoder/decoder conversions and a conversion to text.
The ProxyRepresentation in this case describes only exporting to text, but not reconstructing the profile from it.
Any representation can describe both conversions, or just one.
Now, when we know about ProxyRepresentations, do we really need the FileRepresentation for the video? We could have proxy with a URL.
The difference is subtle.
FileRepresentation is intended to work with the URLs written to disk, and ensure receivers' access to this file or its copy by granting a temporary sandbox extension.
ProxyRepresentation treats URLs the same way as any other Transferable items, like strings.
It doesn't have any of these additional capabilities that we need for files.
It means that we can have both.
The first one, FileRepresentation, allows the receiver to access the movie file with its contents.
The second one will work when I paste the copied video in a text field.
So the URL is treated very differently by file and proxy representations.
In the first case, the actual payload is the asset on disk, and in the second, the payload is the URL structure itself that can point to a remote website.
Another model that I'd like to upgrade is the ProfilesArchive.
There are cases when it doesn't support converting to CSV, and I'd like to reflect that in code.
Let's see.
We add a Boolean property that tells us if we can export to CSV and conversion functions to and from data.
To express this idea in code, we can use .exportingCondition.
If given archive doesn't support CSV, it won't be exported in this format.
With this API, you can even build custom TransferRepresentation, just like custom Views in SwiftUI.
The only requirement is to provide the body property where you can have other representations configured the way you need.
It is useful if you want to reuse a combination of representations, or you have some private data representation that you don't want to expose publicly.
Transferable helped me to quickly build this application with all the functionality that I wanted to have.
I hope it is going to help you building feature-rich apps in less time than ever before.
Thank you for joining me for this session and keep building amazing apps! ♪
-
-
4:36 - Declaring a custom content type
import UniformTypeIdentifiers // also declare the content type in the Info.plist extension UTType { static var profile: UTType = UTType(exportedAs: "com.example.profile") }
-
5:10 - PasteButton interface
import SwiftUI struct Profile { private var funFacts: [String] = [] mutating func addFunFacts(_ newFunFacts: [String]) { funFacts.append(newFunFacts) } } struct PasteButtonView: View { @State var profile = Profile() var body: some View { PasteButton(payloadType: String.self) { funFacts in profile.addFunFacts(funFacts) } } }
-
5:19 - Drag and Drop
import SwiftUI struct PortraitView: View { @State var portrait: Image var body: some View { portrait .cornerRadius(8) .draggable(portrait) .dropDestination(payloadType: Image.self) { (images: [Image], _) in if let image = images.first { portrait = image return true } return false } } }
-
5:27 - Sharing
import SwiftUI struct Profile { var name: String } struct ProfileView: View { @State private var portrait: Image var model: Profile var body: some View { VStack { portrait Text(model.name) } .toolbar { ShareLink(item: portrait, preview: SharePreview(model.name)) } } }
-
6:34 - Profile structure
import Foundation struct Profile: Codable { var id: UUID var name: String var bio: String var funFacts: [String] var video: URL? var portrait: URL? }
-
7:31 - CodableRepresentation
import CoreTransferable import UniformTypeIdentifiers struct Profile: Codable { var id: UUID var name: String var bio: String var funFacts: [String] var video: URL? var portrait: URL? } extension Profile: Codable, Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(contentType: .profile) } } // also declare the content type in the Info.plist extension UTType { static var profile: UTType = UTType(exportedAs: "com.example.profile") }
-
8:30 - DataRepresentation
import CoreTransferable import UniformTypeIdentifiers struct ProfilesArchive { init(csvData: Data) throws { } func convertToCSV() throws -> Data { Data() } } extension ProfilesArchive: Transferable { static var transferRepresentation: some TransferRepresentation { DataRepresentation(contentType: .commaSeparatedText) { archive in try archive.convertToCSV() } importing: { data in try ProfilesArchive(csvData: data) } } }
-
9:14 - FileRepresentation
import CoreTransferable struct Video: Transferable { let file: URL static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .mpeg4Movie) { SentTransferredFile($0.file) } importing: { received in let destination = try Self.copyVideoFile(source: received.file) return Self.init(file: destination) } } static func copyVideoFile(source: URL) throws -> URL { let moviesDirectory = try FileManager.default.url( for: .moviesDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) var destination = moviesDirectory.appendingPathComponent( source.lastPathComponent, isDirectory: false) if FileManager.default.fileExists(atPath: destination.path) { let pathExtension = destination.pathExtension var fileName = destination.deletingPathExtension().lastPathComponent fileName += "_\(UUID().uuidString)" destination = destination .deletingLastPathComponent() .appendingPathComponent(fileName) .appendingPathExtension(pathExtension) } try FileManager.default.copyItem(at: source, to: destination) return destination } }
-
10:05 - ProxyRepresentation
import CoreTransferable import UniformTypeIdentifiers struct Profile: Codable { var id: UUID var name: String var bio: String var funFacts: [String] var video: URL? var portrait: URL? } extension Profile: Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(contentType: .profile) ProxyRepresentation(exporting: \.name) } } // also declare the content type in the Info.plist extension UTType { static var profile: UTType = UTType(exportedAs: "com.example.profile") }
-
11:34 - Proxy and file representations
import CoreTransferable struct Video: Transferable { let file: URL static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .mpeg4Movie) { SentTransferredFile($0.file) } importing: { received in let copy = try Self.copyVideoFile(source: received.file) return Self.init(file: copy) } ProxyRepresentation(exporting: \.file) } static func copyVideoFile(source: URL) throws -> URL { let moviesDirectory = try FileManager.default.url( for: .moviesDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) var destination = moviesDirectory.appendingPathComponent( source.lastPathComponent, isDirectory: false) if FileManager.default.fileExists(atPath: destination.path) { let pathExtension = destination.pathExtension var fileName = destination.deletingPathExtension().lastPathComponent fileName += "_\(UUID().uuidString)" destination = destination .deletingLastPathComponent() .appendingPathComponent(fileName) .appendingPathExtension(pathExtension) } try FileManager.default.copyItem(at: source, to: destination) return destination } }
-
12:57 - Exporting condition
import CoreTransferable import UniformTypeIdentifiers struct ProfilesArchive { var supportsCSV: Bool { true } init(csvData: Data) throws { } func convertToCSV() throws -> Data { Data() } } extension ProfilesArchive: Transferable { static var transferRepresentation: some TransferRepresentation { DataRepresentation(contentType: .commaSeparatedText) { archive in try archive.convertToCSV() } importing: { data in try Self(csvData: data) } .exportingCondition { $0.supportsCSV } } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.