[ Applause ]
Welcome to Session 227, Data Delivery with Drag and Drop.
My name is Dave Rahardja, and I'm joined here, today, by my colleague Tanu, who's going to be operating the demos.
So, if you've been following along in our series of Drag and Drop sessions, this diagram should be pretty familiar to you.
During this session, we're going to be focusing on this part of the diagram, Item Providers.
Now, Item Providers are fundamental to the way data is transferred between applications in drag and drop.
So, in this session, we're going to cover four topics about item providers.
We're going to cover some basics.
We're going to talk about how uniform type identifiers help your apps to be more compatible with others in the system.
We're going to cover how you can create model classes that work really well with drag and drop.
And finally, we're going to close out by covering some advanced topics that'll add some real polish to your apps in our ecosystem.
So, let's get started.
Let's talk about some NSItemProvider basics.
So, what's an NSItemProvider?
Simply put, an NSItemProvider is a data promise.
All data is loaded through drag and drop asynchronously and on demand.
An NSItemProvider can also, help you to provide progress and cancellation of data transfer.
And it is supported widely in our API.
Of course, in drag and drop, but also in UIPasteConfiguration.
And we've added API for UIPasteboard, so that you can use NSItemProvider directly with it.
So, what does it mean to provide data or promise data?
Providing data to an NSItemProvider is really simple.
All you have to do is create an NSItemProvider and pass it an object that is suitable for use.
Now, a lot of our system provided classes, such as UIImage in a string, in its attributed string can be used this way.
And we'll see in a few slides now, how you can make your classes work in this fashion, as well.
Once you provide the data, it can be retrieved by the receiving application equally simply, by calling the loadObject method on the itemProvider, therein.
Remember that data transfer is asynchronous and that your completionHandlers will be called on a nonmain cue.
So, if you're going to update the UI with your received data, be sure to dispatch back to the main queue before you call UIKit methods.
And you're going to see this brought up a few times.
So, to show you how easy it is to use NSItemProviders, we're going to start with a demo.
[ Applause ]
Good morning, everyone.
You've now heard of drag and drop and NSItemProviders.
But now, you're probably wondering how to use these in your own apps.
My name is Tanu Singhal, and I'll show you a simple demo that will help you get started with NSItemProviders.
We have an app, here, that has a list of customers for our product.
Now, I met some new people who are also interested in trying out this product.
So, I noted their names down in Reminders.
It would be nice if could drag a name and drop it into my app.
So, how do we do this?
Let's look at the TableViewController for our Contacts app.
Before I start typing I want you to know that we made the sample code for this available on the developer website.
So, you don't have to worry about jotting down everything, right now.
In the TableViewController, I'll add a TableViewDropDelegate.
Let's also set this delegate.
You may have heard of the TableViewDropDelegate in previous sessions.
But if you haven't, then don't worry.
Because for now, we only need to implement one method called TableView performDropWith coordinator.
In this method, I'll iterate over the drag items provided by the ItemProvider.
Sorry, the dragItems provider by the coordinator.
Now, each dragItem has its own itemProvider.
And we can check whether the itemProvider can load objects of a certain type.
So, I'm going to check if the itemProvider can load a string.
For that, I'll call the canLoadObject method.
If it can load it, then I'll call the loadObject method.
And the completionHandler of this loadObject method will give me an object which will be a string that I can use to update my data models, as well as my UI.
One thing to note is that the completionHandler is on the background thread.
So, if you want to make any changes to your UI, then you need to go back to the main thread, and then make those changes.
So, let me dispatch my main queue.
And in the main thread I'll use the string object, and I'll insert it into the TableView.
This code is ready to run, now.
Wait just a moment.
And this time, I can drag a name from Reminders and drop it into my app.
The canLoadObject and LoadObject methods can also be used for other system types, like attributed strings, URLs, images, and even colors.
So, now that you've seen how straightforward it is to handle system objects in your using itemProviders, I would encourage all of you to try out these APIs.
Up next, Dave will talk about Progress and Cancellation.
[ Applause ]
Thank you, Tanu.
So, that covers the basics about how to use NSItemProviders in your application.
It was really simple.
Now, let's talk about progress and cancellation.
When you retrieve data using NSItemProviders, we return a progress object to you that allows you to track the progress of data transfer as it occurs.
The progress object has two interesting properties on it, that you might want to observe.
The first, is the fractionCompleted property, which tells you from zero to one, how much of the data has been transferred.
And the second, is an isFinished property that tells you that the data transfer has been completed, whether it is successful or not.
You can use key value observing to watch these two properties and update your UI accordingly.
Such as by providing your own progress indicator and cancellation button.
There is a cancel method on the progress object that will immediately cancel the data transfer, as well.
When you call he cancel method on the return progress object, your receiving application will receive, will get a callback on the completionHandler with an error in it, regardless of whether or not the source application has cancelled their data providing.
You get one progress object per load request.
However, there's an overall progress object that you can get from the UIDropSession that allows you to monitor and cancel the entire remaining data transfer in progress.
So, now that we've covered the basics, let's talk about maximizing compatibility.
You want your application to provide data to as many other applications on the system, as possible.
And receive drops from as many applications, as possible, as well.
The key to understanding how to maximize compatibility is understanding uniform type identifiers.
So, let's recap.
One NSItemProvider represents one thing that the user is dragging across the glass.
However, there can be multiple representations that you can make available for every item that the user is dragging.
Let me give you an example.
If you're writing a vector drawing program, for example, you could offer a native file format for best quality as the thing that you're dragging.
But you might also offer conversions to PDF, PNG, or even JPG, so that you can drop this item on a variety of destination applications.
Uniform type identifiers allow you to tag these representations with unique strings that identify the kinds of data that you're putting up for the drag.
For a native file format, you can define your own strings, such as com.yourcompany.vector-drawing.
And for commonly known types, such as PDF and PNG, you can use the symbols defined in mobile core services to tag them.
Now, fidelity order matters.
The order in which you register your representations should correspond to the order of quality that you're making available to the destination.
Usually, the highest fidelity would be your internal data representation type.
And then, followed by the next high fidelity common type, and so on and so forth.
Now, one tip to remember when using type identifiers to register your representations, is that there are uniform type identifiers that correspond to abstract types.
Such as data, or plain text, or image.
Instead, use concrete data types that help the consumer of your data interpret the bytes that you're going to send over to them.
So, for example, use utf8-plain-text, instead of plain-text.
Or probably .png instead of image, so that the receiver can interpret your data.
Of course, for your private type identifier, you're free to define your own data types and byte layouts.
So, you've seen how these multiple representations allow you to maximize compatibility.
But you didn't see any of this type identifiers being used in our initial example code of creating NSItemProviders.
So, how did this work together?
This is where we talk about creating model classes that harness the power of both multiple representations for compatibility.
And the simplicity of using objects to initialize itemProviders.
To create model classes that work well with drag and drop, we're going to talk about two protocols that help you to do this.
NSItemProviderReading and NSItemProviderWriting.
Let's talk about the writing protocol, first.
NSItemProviderWriting exports data from your model object and NSItemProviderReading imports data and creates a model object from a representation.
And by adopting these protocols, you can maintain the conversion between your model objects and multiple representations with your model object and not with the UI code.
So, let's take a look at NSItemProviderWriting.
This is what it looks like.
There are only two things that you need to implement to conform to this protocol.
The first is a writableTypeIdentifiersForItem Provider property.
This is the list of the type identifiers that you can export in fidelity order.
Highest fidelity first.
And then, a loadData method which will take that typeIdentifier that has been requested by the other application.
Which will call a completionHandler, either with the data that you've made available or with an error.
By implementing this protocol, the UI code can look like this and our framework code will do the equivalent of this, for you.
Let's take a look at the reading protocol.
The reading protocol is, of course, the other end of the pipe.
It too, has two things that you need to implement.
The first is the list of readable type identifiers in fidelity order, also.
And the second, is that initializer that will take a NS data block, a NS data object, which should be used to initialize the instance of your object.
When you implement this protocol the UI code can look like this, a canLoadObject.
You can pass it to your vector drawing object.
And when you call the loadObject, our framework code will matchmake the two lists of type identifiers, both from the provider and the consumer.
To find the best match to ensure that the highest quality data is used to create an instance of your object.
So, in short, to make model classes that work well with drag and drop, you should create, these classes should conform to NSItemProvider reading and writing protocols.
Because these are Objective-C protocols, your model classes should also inherit from NSObject.
And when you do this, you can use your classes and your objects wherever NSItemProvider is supported, drag and drop, and UIPasteConfiguration, and also, with the new API in UIPasteboard.
So, to show you how this is done in code, I'm going to ask Tanu to do another demo.
[ Applause ]
In the previous demo, we learned to load simple system objects like strings.
Now, let's see how we can drag a rich contact card from the Contacts app and drop that into our app.
To do this, I've created a class called ContactCard.
This holds the name, phone number, and photo for a contact.
Now, I'm going to go back into the TableView controller, and this is the performDrop method that we implemented, before.
Last time, we were trying to load a string.
Now, we want to load a contact card.
So, I'll replace all instances of NSSTring with ContactCard.
Since we are requesting a contact card the completionHandler will directly give me a contact card object, and I can use that to insert into my TableView.
Now, this should have worked, right?
But the reason we're getting errors, here, is because the NSItemProvider does not recognize my ContactCard class.
So, we need to do a little work to conform to the NSItemProvider.
Let's look at the ContactCard class, again.
And here, I'm going to implement the NSItemProviderReading protocol.
This will tell the NSItemProvider that I can read data that it provides.
Now, I need to specify the types of identifiers that I can read.
My ContactCard class can read either a vCard, or it can read plain text.
Note that these need to be specified in descending order of fidelity.
If we specified plain text first, then we would always get called back for plain text.
Even when a more rich vCard type was available.
So, after we've specified the types we can read, we're going to set up an initializer where we'll actually read the data from the itemProvider.
One of the arguments in this initializer is the typeIdentifier.
So, I can use that to figure out what type of data I received.
If I got a vCard, I'll use a helper method to set the phone number, picture, and all the other information I need.
If I just got plain text, I'm going to set the name for my contact card.
Now, when we run this, those errors are resolved.
This is because the NSItemProvider understands our class, now.
So, this time, I can drag and name from the Contacts app on the right, and drop it into my app.
As you can see, we loaded the name, phone number, as well as the photo for this contact.
Note that we can still drag out plain text, like from the Reminders app, again.
And this works, because our ContactCard class not only handles vCards, but it also handles plain text.
So, this is what we wanted.
And our users really like this feature.
But now, we have another feature request.
They want the ability to drag out contacts from our app and drop it into other apps.
In order to implement that, we'll have to conform to another protocol called NSItemProviderWriting protocol.
As part of this protocol, I'll specify the types of identifiers that I can write.
My class can again, write either a vCard or plain text.
After this, we need to specify a loadData method.
In this method, we'll create the data that we want to provide to the itemProvider.
So, based on the type identifier, I can create different types of data and I'll just pass it to the completionHandler.
Now, we have completed the implementation for the NSItemProviderWriting protocol.
There's just one last thing to do.
We need to go back into our TableViewController and tell the TableView that it can be used to drag out items.
To do this, I'll implement the TableViewDragDelegate.
We need to set this delegate, too.
And for this delegate, we'll implement one method called itemsForBeginning session.
In this method, I'm going to create an itemProvider.
And you can see that I'm able to pass a ContactCard object directly to the itemProvider's initializer.
This works only because we implemented the NSItemProviderWriting protocol.
Let's run this code, now.
And I would like to share a contact with my colleague on Messages.
So, let me drag out Dinesh's contact and drop it.
As you can see, we were able to send out the name, phone number, as well as the photo.
Which is what we were trying to accomplish.
So, now we have learned how the NSItemProvider reading and writing protocols can be used to transfer data using custom class objects.
These are really powerful protocols and we think you'll find them extremely useful when you implement drag and drop in your own apps.
Up next, Dave will cover some advanced topics.
[ Applause ]
So, cool. All right.
Next, we're going to cover some advanced topics.
These are just some collection of things that is very good to know, when you're going to polish your app so that it shines in the drag-and-drop environment.
The first thing we're going to cover is data marshaling.
So, if you look at the NSItemProvider API in iOS 11, you'll discover that there are three ways you can provide data.
You can provide data as a data object, obviously.
But you can also provide data as a file or a folder on your file storage.
Third, you can provide the data as a reference into a File Provider.
We'll talk about File Providers more, later.
Similarly, the receiver of the data can retrieve data in three different ways.
They can copy it as their own NS data object.
They can copy a file or a folder into their container.
And they can attempt to open the file in place.
So, three ways to provide data, and three ways to consume it.
What do you have to do to make sure that data transfer happens seamlessly, and that the data is made available in the correct format?
Well, the good news is you have to do nothing.
We do it for you.
Data marshaling makes sure that the provider of the data can provide and consume the information in the way that's most convenient for them.
If you provide a file and then, you consume it as a data, we will read the file into an NSData object for you.
If you provide an NSData and you ask for a file copy, we'll write it to file storage and give you your L reference to it.
If they provided a folder and you asked for NSData, we will zip up the contents of the folder and give you an NSData of the zip file.
And if they provide a reference to a file provider and you ask for a file copy, we will call in the promises for the file provider and make a copy on your behalf.
So, we've seen how progress and cancellation worked from the consumer side.
So, I want to talk a little bit about how progress and cancellation works on the provider's side.
I'm going to show you a block of code, here.
Don't worry that it's a dense block of code, because I'm going to highlight the parts that are most important.
This is a loadData implementation for our NSItemProviderWriting.
In which you are going to use a dataLoader object to incrementally load data.
To provide progress and cancellation support, the first thing you have to do is create your own progress object.
In this case, we're creating one with a UnitCount of 100 as a percentage.
On that progress object, you can attach a cancellationHandler that will be called when the consumer of your data calls cancel on their instance of the progress object.
In this case, all we're doing is setting a local variable from true to false, which should stop the dataLoader from loading the next chunk.
As the dataLoader progresses, you can update the completedUnitCount on your progress object to drive the progress indicator on the receiver.
And of course, you have to return your own progress object so that we can type it over to the other side.
Switching gears, now.
We're going to talk about some features that help you to polish your data representations when creating suites of apps.
So, we're going to talk about Per-Representation Visibility.
Remember, you can provide multiple representations of your data in NSItemProvider.
You can restrict the visibility of each representation to either visible to only your source application.
To the applications that are assigned by your team, so in your suite of apps, or to everyone.
You can use this property to hide private types that you are rapidly maintaining with versions of your application suite.
So, you don't have to worry about third parties serializing your data to disc.
In a similar vein, you have a team data property on NSItemProvider.
This is an eight kilobyte data block that you can attach through an NSItemProvider.
They are only visible to other applications in your team.
You can use this to improve your UI in any way you see fit, during a drag.
This is metadata, and can be retrieved even before the user lifts their finger.
There is a suggestedName property on the NSItemProvider.
If you provide a string for the suggested name, we will use it as a file name when the retriever writes your data to disc.
This is especially useful, of course, when you're providing an NSData.
And the last property I want to talk about is Preferred Presentation Size.
This is a CG size property that you can provide to give a hint to the receiver about how big your representation's going to end up after they've laid out.
This is used, for example, by Mail, to drop images into the Mail Compose sheet, into the destination layout, even before the data arrives.
And finally, I'm going to talk about File Providers.
So, File Providers is a whole other topic, on its own.
But a short summary can be said as follows.
A File Provider is an app extension.
This app extension allows data transfer from a network download, for example, to continue even if your main application has been terminated.
For long running drags, this is especially useful.
Because as the user waits for a download to occur, they may navigate away from your app, and your app could get terminated.
If you provide a URL to a file inside a File Provider container, the File Provider extension will continue to serve the data transfer request, even though your UI application is terminated.
And that increases the chances that your data is going to be transferred successfully.
And as an added bonus, if you create a File Provider that appears in the file's app, we will allow you to drag and drop URLs that can be opened in place.
Which means that multiple applications can access that same file, instead of getting their own copy.
There's some really great information available in the following two sessions.
I highly recommend that you attend them, if you are interested in providing File Providers in the context of drag and drop.
Next, we're going to take a look at what we can do with files in drag and drop, by having Tanu do another demo.
[ Applause ]
Hope you are ready for another demo.
In the previous demo, we saw how we could drag and drop simple system objects, as well as custom class objects.
Now, let's see how we can handle files.
We'll look at our customer's app, again.
And for some people, here, we have additional data.
Like for Adam, we know what products he's purchased.
Now, I would like to drag out this file into another app and potentially edit it, there.
To implement this, I'll look at the ContactDetailsViewController in my customer's app.
Over here, we will add a drag interaction to the attachment image that we just saw.
Let's also, implement the Drag Interaction Delegate, right here, because we have set the delegate to self.
You may have heard of the drag interaction delegate in previous sessions.
If you're interested in learning more about this delegate, I would encourage you to check out the video for Session 213 on Mastering Drag and Drop.
For the purposes of this demo, though, we'll only look at one method, called itemsForBeginning session.
In this method, we'll create a new itemProvider, and on the itemProvider we'll call a method called registerFileRepresentation.
This method takes four arguments.
The first one is the type identifier.
Our attachment file was actually an image.
So, I've set the type identifier to JPEG.
The second parameter is file options.
This can be used to specify if we want other apps to open our file in place, or if we want them to make a copy.
We would like them to open our file in place, so we can see the changes they make.
The third parameter is visibility.
This can be your own process, your team, or all.
I've just set it to all.
Finally, we have the completionHandler.
And we are going to pass the URL file into this completionHandler.
So, let's get the URL, first.
Over here, I have a helper method that grabs the URL for my file from a File Provider.
I need to use a File Provider only when I'm opening files in place.
If I wanted other apps to make a copy, then I do not need a File Provider.
I could have just provided a local URL.
We'll pass this URL to the completionHandler.
Now, our itemProvider is set up.
And we can use this to create a drag item.
With this, the code is ready to run.
So, now I can drag out this file.
And let me open another app where I'll drop it.
So, we'll drag out the data, drop it into a document editing app.
And I'm concerned now, that Adam is buying a lot of products from our competitor.
So, let me highlight this and save these changes.
So, since our document editing app actually used the URL provided by the customer's app, you'll see that we got the same changes back in our customer's app.
[ Applause ]
Glad you like that.
We've now seen how to handle system objects, custom class objects, as well as files.
We have a wide range of APIs that you can adopt.
And we look forward to seeing how you use them.
Back to Dave, to summarize.
[ Applause ]
Thank you, Tanu.
During this session, we've seen how an NSItemProvider is fundamental to the way data is transferred between applications in Drag and Drop.
We've seen how multiple representations allow your applications to maximize compatibility with other applications.
And we've seen how data is transferred asynchronously with progress in a cancellable fashion.
We've also seen how NSItemProvider reading and writing protocols allow you to make model classes that work well with drag-and-drop UI code.
And how visibility and team data allows you to create suites of apps that work in especially well together during drag and drop.
And finally, we've seen how File Providers afford you the ability to open files in place as the result of drag and drop.
For more information, please visit our developer website, Session 227.
And I highly encourage you to attend the File Provider related sessions, today and tomorrow, to learn more about how to create File Providers for your own applications.
If you haven't seen the first three sessions on Drag and Drop, I highly recommend that you review them on video.
Thank you, for your attention, and have a great WWDC.
[ Applause ]