App Frameworks • iOS • 55:52
Dive deep into the new Drag and Drop APIs in iOS 11. Learn what users will come to expect of your draggable views and how to best deal with the asynchronous nature by which data gets dropped into your app. We’ll also show you how to make your Drag and Drop look great using the advanced visual appearance tweaks that we offer.
Speakers: Tom Adriaenssen, Wenson Hsieh, Robb Böhnke
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
Good morning. I'm glad you could make it. My name is Robb, and together with my coworkers, Wenson and Tom, I'm going to take you on a deep dive today through the new eight drag-and-drop APIs we are introducing in iOS 11. So, we have a bunch of new APIs for you, but I don't want you to get intimidated.
Even though we have a lot of ground to cover, you can gradually adopt these APIs and, in fact, if you are one of the few people who use Collection view or Table view, there is a dedicated session for you tomorrow in Hall 2 that you can check out, and it's followed back-to-back by one on NSItemProvider. If you ever wondered what the U in UDI stands for, do not miss this session.
This session, however, is going to split in two halves. First, we're going to talk about the drag side of things, the drag interactions, it's delegate, the session, the associated drag item and previews, and then Tom is going to take over in the second half, and do a similar thing for the drop side. So, we have a lot of stuff to talk about, and we're going to start with advanced drag interactions.
So, as you already know, drag and drop on iOS is not only a way to share data between applications, it's also a fluid user interaction, and with these kinds of complex user interactions, consistency is key, and you achieve this consistency by using UIDragInteraction. You can take one of those and install it on any of your views. You don't have to subclass anything, or even worse, go in and change the existing superclass of the custom views.
Just install one of them, and it will do the necessary gesture bookkeeping for you. You do, however, have to implement UIDragInteractionDelegate, which at least return a UIDragItem. You can [inaudible] into a bunch of notifications about lifecycle and perform some animations, and we'll go over those in a second.
If none of this makes any sense to you, I would recommend you catch up on Introducing Drag and Drop, which is a session we had yesterday, so if you missed it, you will have to check out the video, and that will kind of cover the basics. However, if the basics were good enough, you wouldn't be here today, so let's look at some of the behaviors that native applications in iOS 11 employ, and what your users will come to expect of your applications, going forward.
So, here you see me in Mail, and you can see that, as I start a drag with a long press on a message, I can then tap subsequent messages and they will flock to the drag session that I have already in progress, so the little batch count keeps increasing, there. This is not something you get out of the box for free, but it's not very hard to implement, either, and I'm going to show you how.
So, in this example, I've already implemented UIDragInteractionDelegate, specifically the required method dragInteraction itemsForBeginning session, in which I create an NSItemProvider for the message I want to drag, then I create a UIDragItem with said itemProvider set to localObject to the message, so I can later easily refer back to the message, and then I return the dropped-in array. Now, you could use the exact same implementation for dragInteraction itemsForAddingTo session withTouchAt point, and that would work. However, there is some edge cases that I want you to think about as you implement flocking by opting into this method.
First, if you implement this method, your dragInteraction can now flock with any other dragInteraction in your application, not with dragInteractions in other applications, because we currently don't allow cross-application flocking, but that dragInteractionDelegate that Bob's implementing in that other department, you can potentially flock with that, so you want to be aware.
In this case, I decided that I only want to flock my messages with other messages, so I iterate over all the items in the session, and if any of them does not have an NSItemProvider that has a UDI conforming to this type, in this example, private.example.mail, I'll abort by returning an empty array, and this will give other gesture recognizes to recognize the tap that triggered this flocking attempt.
Similarly, by default, the same dragInteraction, therefore the same UI view can flock multiple times. We can't possibly know if that makes sense for your application, or if maybe different regions inside your view correspond to different drag items, so you have to tell us. And, in this case, I check if the local object of any of the other items already in the session correspond to the message that I want to check now, and if it's already there, I also abort, using an empty array. So, this is how you get flocking.
Another behavior that Mail has is this. As I long-press on a thread, instead of lifting one item that represents the thread, one item, I instead lift three that each represent a message in this thread, as indicated by the little blue bubble, there. As we've already seen, UIDragInteractionDelegates drag item itemsForBeginning session returns an array, so it stands to reason we can, in fact, return multiple items here, and then that's something I'll do here. So, I grab all the messages from my mailThread, I sort them, and I'll explain why in a second, and then I return a UIDragItem for each of them in the exact same fashion that you've seen before.
Now, the reason that I'm sorting them is that the order of the array that you return here matters, and it's so that the last item in the array is going to be the topmost object of your lift, and since I want the newest message first, I sort them so that the oldest is the first in the array.
And, this would work. However, if this was all we implemented, all the items would lift with the same preview. That means they would have the same visual representation and, by default, the preview that we create is going to be based on the view that the interaction is installed on, so that would mean that all three messages would have the thread as their visual representation. And, since I don't want that, I implement the optional dragInteraction previewForLifting item session, in which I get to return my own drag preview.
So, what I do here is, I first attempt to find the message associated with that item, and if I have it, I grab its associated message views through its helper method I happen to have, and initialize my UITargetedDragPreview with that. And, that would mean that all of the messages that I'm lifting would have their own representation based on the message view dedicated to them.
And, last but not least, here's another thing that Mail can do that is kind of tricky. You see me here dragging photos from Photos to Mail, and if you pay attention, you'll notice that the photos lift into an appropriate size for the entire width of the compose sheet.
So, how is it that Mail already knows how much room to make, and where to target the preview? Is it because the data just happened to arrive so quickly? No. You should not make any assumptions here. In my demos, the data's going to arrive in time, but that may not be the case for your users, and there's a better way to handle this situation.
It turns out that NSItemProvider has a property called preferredPresentationSize, which allows you to communicate the size that you expect something to be represented at on the other side, kind of out of channel, so even though I'm initializing the NSItemProvider with the file here, I happen to know the size, and I can set its preferredPresentationSize accordingly, and then Mail is able to read that out on the other side, and everything else is just the same. Now, you can take my word for it that this works, but we've only known each other for what, nine minutes? So, Wenson's going to show you a demo now.
[ Applause ]
- Alright. Thank you, Robb.
- So now, before I jump into the demo, there's a couple of things I'd like to say. All of the sample code that I'm about to show you will all be available online, and I strongly encourage you all to check it out. Second of all, we will be going over not one but two demo apps today. First one is called Drag Source, and it will focus on drag interactions. Second one, Drop Destination, will focus, as you might have guessed, on the drop side. So, with that said, let's take a look at our first demo app.
So, in here, we see four stacks of images, and currently, we have implemented very basic cases of drag interactions, so we're able to drag a single image out of each stack. So, that works great, but it would be kind of cool if we could drag an entire stack of images out as individual items, one per each image. So, let's take a look at the code and see what we can do.
Now, currently, if you look at this, we just consider the last image view, and use it to create a new drag item, just using that last image view. Instead, it's a pretty short stretch to enumerate through all of our available image views and return drag item for each one. So, we're going to do just that, and now let's see how it behaves. So, watch what happens when I begin a drag on the second stack of images.
You'll notice that there's a badge count of three this time. That indicates that there are three items in the drag, corresponding to the three images in the stack. Now, if I bring up Photos on my right side, here, you'll notice that I can actually drop these three images into Photos, and it'll save them as individual items. Now, while we're in Photos, there's something else I'd like to show you.
So, I've begun a drag on one of these images, and now, if I tap on these other two images, you'll see that we add those to the existing drag session as well. So, as Robb mentioned, this is not a behavior we get for free. Luckily, it's pretty easy to implement, and I'll show you how. So, going back to the code, all we've got to implement is itemsForAddingTo session.
So, the thing to notice here is that we can actually use the exact same logic to construct drag items in itemsForAddingTo session as in itemsForBeginning session. So, to make this easier for ourselves, we'll just take the logic that used to have in itemsForBeginning session and introduce a new helper method. I'm going to call this dragItems for session.
Then, in both places, when we are adding to an existing session, right here, and when we are creating a new session in itemsForBeginning session, we'll simply turn around and call this new helper. OK? So, that should give us the ability to add more images into our existing drag session, and as you can see, as I tap on each of these other three views, we're able to add all 10 images into the drag session. So, that works pretty well, but there's one caveat. So, notice here, I'm going to start a drag on the first stack of images, and I'm just going to keep on tapping the first stack of images.
So, you can see that I'm able to arbitrarily add a whole ton of images that I probably shouldn't be able to add. This seems like a bug to me. I have 26 copies of each image now in the drag session. So, let's figure out how we can fix that.
Now, here's our helper that we just introduced, and what we can do here is, instead of using all the imageViews to create dragItems every single time we tap, what we're going to do is filter out the imageViews, so we don't use an imageView to create more than one dragItem.
So, with those three lines of code, I'm going to hop back into the app and show you how it works. OK. So now, I've begun a drag on the first stack of images, and I'm going to tap the next three stacks. Now, watch what happens when I try to add more items.
Our bug seems to be fixed. We can no longer add redundant items to our drag session. So, these were just some basics for manipulating the drag items that we supplied through our delegates. I'd like to now hand it back to Robb to discuss some of the more advanced techniques for customizing animations and drag previews.
[ Applause ]
So, let's add some polish to our previews. One thing that's often the case is that the view that you want to lift is not quite ready for prime time, so maybe there's some highlighting state, or you have some overlay that you want to fade out. And, the lift is actually a great point to do that, because during the lift, the view is still live, so any changes you make inside of that view will be reflected during the animation, and it's only that, at the point where the user starts moving their finger that we perform a snapshot, and that state is what the user will see for the rest of the drag interaction.
The way you could animate alongside the lift is like so. There is an option delegate method, dragInteraction willAnimateLiftWith animator session, in which you get handed an animator object, and here I'm going to just grab all the messages that I have on the items in the session, and find their associated messageViews. So now, I have an array of messageViews, and for each of them, I will just add an animation to the animator in which I fade out an overlay by setting its alpha to zero.
And then, in the completion block, I will set the alpha back to one, and what that will do is that as the view lifts up, the overlay will fade out. If the user lets go and the view settles back into place, the overlay will fade in, because the animator is able to automatically revert this animation. Then, when the drag starts, we will snapshot the view, and after that, the completion block will get called, and the overlay will be reinstalled. So, that means in the snapshot, there won't be an overlay, but in the view that remains inside the application, there will be.
But, what if the view that you're lifting, the view that you're lifting is not the view that the interaction is installed on, or what if the view isn't square, or what else can we do? So, we already saw that UIDragPreview can be initialized with the view, but there are two other parameters, and I'll go over each of them individually. The first is a parameters object that allows you to customize the appearance, and the second one is a target that's used for positioning.
So first, the parameters. That is an instance of UIDragPreviewParameters, and it has two properties. The first is a color, and that's going to be the background color of the view that we will install behind your view, because a lot of views aren't actually fully opaque, and it would look not so good if we just lifted them as they are.
However, you get to customize this color in any way you want. You can make it black, or clear. You can really go to town, here. The second property is a little more complicated. It's a UIBezierPath that lets us know what the visible region of your view should be, so if your view is not square, you could set a rounded rectangle here. But, there are some things to be aware of.
So, by default, if you don't supply drag preview parameters, so you don't set a path on the drag preview parameters you supply, we will lift the entire view. If you wanted to crop out the subrect phon, so in this case, the rounded rectangle with the kid in it, you could supply a Bezier path, and it would result in something like this.
So, it's important, however, that the Bezier path that you supply has to make sense within the coordinate space of the view. So, in this case, the bounds that I initialized this rounded rectangle with have an origin that is relative to the origin of the container, as indicated in gray, sorry, as, the origin of the view, as indicated in gray, so that's the top left corner.
And, you want to also make note of the midpoint, because it's the midpoint of the visible path that we'll later use for positioning, when we talk about target. So, this is how you would get this kind of preview. However, you're not limited to giving us a path that is smaller than the view.
You can also give us one that's bigger. So, in this case, I chose an origin that is negative, and it works in the [inaudible] of the view, and that would result in this kind of platter that frames the picture. And, the color you see here is in fact a background color that defaulted to white. Now, if you're bold enough to implement your own text rendering, there's a dedicated, thank you, style that you can use to match the way that we lift text, so you want to refer to the documentation for that.
And, the target, so the target is used to position a transitory view that we will use to perform the animation with inside your view hierarchy. If you don't supply your target, we will infer one based on the superview of the view that you provided. That means if you provide a view as your view in the direct, in the targeted direct preview, if that view is not in the view hierarchy, you will have to supply your target. Otherwise, we can infer one. This UIDragPreviewTarget has three parameters. The first is the container. This is where we're going to install the view, so you want to be aware of any add or remove subview calls in that container.
And, the second one is a position, and the third is a transform. The transform is only relevant on drop, and it allows you to rotate or scale on set down. The position, however, is a little more tricky. So, as I mentioned, if you give us a point in your container, as indicated in gray, by default we will center the midpoint of your view around this position, so if you don't supply a visible path, it would look like so.
However, if you do supply a visible path, then as I said, the midpoint of the bounds of this path will be centered around this point. So, it's no longer the midpoint of the view. It's the midpoint of the visible path. And, it also means that if your path is a little bit more complicated, such as this one, where I just unioned two rounded rectangles together, the midpoint is now not even in any of the two rectangles. It's still the midpoint of the enclosing bounds of both of these shapes.
But, if you already had a chance to look at iOS 11, you have noticed that a lot of the apps in the system are actually able to update the preview after the lift, so here you can see Maps, and as I move this little Apple Park cell around, it gets replaced by this little map snippet after the fact.
So, how can we do that? Well, it turns out, there's a second preview class in the systems, next to UITargetedDragPreview, and that is UIDragPreview which, as you might have guessed, is very similar to UITargetedDragPreview, but it doesn't have a target. All the other semantics still apply, and the view that you initialize this preview with may or may not be in the view hierarchy. It's not relevant anymore, at this point.
But, how will you update this preview? First, you want to find a spot in your session lifecycle where it's appropriate. So, in this case, I chose sessionDidMove, and what I want to do here is that as the user moves out of the listView in my hypothetical Mail app, I want to replace what they're dragging with a little envelope graphic. So first, I perform a hit check to see if I'm still inside the listView, and if I am, I just abort by returning nothing. And then, I iterate over all the items that have a message as their localObject.
I check if I have already updated this item, because this operation is not free, and sessionDidMove may get called quite frequently, but if I haven't, then I will set the previewProvider, and this is a block that we will later call to update the preview, and inside the block, I first create an imageView with the image I would like, and then I initialize a new drag preview with this, and it's important to realize that we may not actually call in this block.
So, if you are lifting many messages, we may not, we may decide not to display all of them, and we wouldn't bother calling in a preview block for the views that we don't actually show on the screen. And, last but not least, I have to do some bookkeeping. So, theory is still second to practice, and Wenson's going to give you another demo, and I'll see you in the last. Thank you.
[ Applause ]
Alright. Thanks again, Robb. So now, I'd like to introduce the second example we are going to be looking at in Drag Source. So, check this out. When I drag on this image of two QR codes, we have a drag session that contains two items. What are these two items? Well, if I drop it in Photos, we'll see that it's actually the cropped images of the QR codes.
So, I've gone ahead and detected, where are the QR codes are already in this image? Now, the thing we can polish here is the drag preview. So, we haven't done any customization yet. And so, by default, we use the entire image view to represent either of the items, either of the QR codes.
That is, we are actually seeing the entire image view twice, two of them stacked on top of each other. It would be kind of cool if we could use just the cropped image of the QR code as the drag preview as we are lifting, and when we are dragging them around. So, let's take a look at what we need to do this.
First thing we're going to do is implement previewForLifting item, so in here, we're going to take some information about the QR code, namely the cropped image of the QR code, as well as some geometry describing where it is in the image, and we're going to use it to create a new UIImageView.
Then, we're going to create a drag preview target and drag preview parameters. Note that right here we set the visiblePath to a new UIBezierPath that's a roundedRect, and that will give us a nice rounded preview. So, we combine all of this information into a new targeted drag preview, and with this change, we should see a much more polished drag preview when we lift.
Now, check out what happens when I begin to lift. So, instead of the entire image view popping up this time, we see individual rects for the QR codes get lifted up, and as I drag, you can see that these are the two QR codes flying around, so that looks pretty good, but there's something that looks kind of weird, and I'm about to show you. Watch what happens to the QR codes when I let go.
Now, I've let go somewhere that doesn't actually accept the drop, and so we'll do a cancel animation. The problem is that we haven't actually told UIKit where the drag preview should animate to when we cancel. So, let's fix that problem. We're going to do that by implementing previewForCancelling item.
This looks and works a lot like previewForLifting item. In fact, observe that if we want the QR codes to go back to their original locations, what we can actually do, similar to what we did in the first example, is take our code that used to live in previewForLifting item and factor it out into a separate helper.
So, we're going to call it dragPreview for item, and what this is going to do is return the original location of the QR codes in both the places where we are lifting and when we are cancelling. So, I'm going to call that helper in these two places really fast, and rerun the application.
Alright. Now, let's see what happens when I cancel. And, see that they fly back to their original locations and then settle down. So, that looks so much better than it did before, but there's one more thing I'd like to show you. So, we're going to hop on over to the right side, where we have Photos, and you can notice that, as I drag some images, we'll fade out the background of the image views to kind of indicate that we are currently dragging an image from that view. There are a lot of apps around the system that do this, and we can certainly get the same effect in our own demo app. So, I'll show you how.
What we're going to do is implement a few alongside animations. So, as we animate the lift, we get this animator object that we're able to attach alongside animations on. So, we're going to add this new block that sets our alpha to 0.5, our alpha being the alpha of the overall image view.
So, we're going to fade out the image view as the lift is happening, and as the lift is canceling, I'm sorry, as the drag preview is canceling, we are going to revert the alpha to 1, so we're going to fade the view back in. Now, it would be kind of a shame if our alpha were permanently ghosted at 0.5, so when the drag session ends, we've got to be careful and set our alpha back to 1, to make sure that we're at full opacity when the drag finishes.
So, with those changes, I've rerun the app, and now watch what happens when I begin the lift. You can see, this nicely indicates exactly where the QR codes are by fading out the rest of the image view, and as I cancel, you notice that the rest of the image view fades back in just as nicely.
So, that's all good and polished. Let's look at our third example. This is Draggable Location Image View, and in here, the trick is that we're adding not only the image as a representation to the item providers when we start a drag, but we are also adding the location.
What that means is that I'm able to drop into an application that accepts location, such as Maps, at it will actually navigate me and drop a pin at the location where this photo was taken, which is, of course, the Golden Gate Bridge. So, that looks pretty good, except for one thing.
Now, when I begin a drag, I haven't done any customization around the drag preview, and so this default drag preview, which is the entire image view, doesn't do a really good job of really highlighting the fact that we have a location, and not just an image. It looks just like we're dragging an image right now.
So, let's fix that. Now, we're going to go into Draggable Location Image View. This is where our logic is going to live, and we're going to implement sessionWillBegin. So, what do we want to do when the session is about to begin? We're going to take our drag item that we've created, and we're going to set the previewProvider property to a new block.
Now, in this block, what we're going to do is create a new LocationPlatterView. This is just a custom view I wrote that knows how to represent both an image, as well as some text describing the location of the image. And, we're going to create a new UIDragPreview using this information.
OK. So, with that change, we should be able to see a much nicer, hotter representation for our drag preview. So, watch what happens when I begin a lift. Now, interestingly, there's actually no difference. The reason is because we put our logic into sessionWillBegin, and the session does not begin until I actually start moving my finger. So now, I'm going to start moving, and look at that. The drag preview has now morphed into this platter representation that shows both the image.
[ Applause ]
That now shows both the image, as well as the location, and as always, I'm able to drop into Maps. It'll navigate me and drop a pin there. So, we've discussed a number of the advanced techniques on the drag interaction side of things. I would like to now hand it to my other colleague, Tom, to discuss some of the advanced APIs used to customize drop interactions.
[ Applause ]
Thanks, Wenson, for finally dragging me into this. No, I'm happy to be here. So, let's talk about the drop side. Let's take a deep dive into a drop. We're going to talk about drop sessions first, and that will bring us to actually performing a drop. So, what is a drop session? It's the other side of a drag session. It gives you access to everything related to a drop.
You can get access to the drag location where the user is dragging inside your view. It gives you access to the items in the view, what type of data is there, and actually their data in the end. It gives you access to the configuration, so you can act appropriately. And finally, it gives you access to the drag session itself when you're dragging locally, but more about that later.
One thing to keep in mind about drop interactions is that only one interaction would handle only one active drop session at the same time. Why is this? Because, it would fit most use case scenarios. That means that once the user is dragging around and enters your interaction, any other session that comes around and tries to enter your interaction won't be picked up. Remember, you can drag with more than one finger at the same time.
Now, if you don't want this behavior, and you do want more than one session to be active on your view at the same time, there's a few options. You can add more interactions, just add a few more drop interactions, and they will all be handled at the same time.
They can have the same logic, or they can have different logic. Doesn't really matter. Or, there's a property you can set on the interaction called allowSimultaneousDropSessions. Set it to true, and that will lift the block on only one session, and you can handle more than one session at the same time. But, your delegate has to handle this properly.
Let's talk a bit about how a drop works. We've been over this yesterday in the introductory talk, but let's gloss over it. User is dragging something, it's approaching your view. Before we do anything, we'll call canHandle session on your interaction delegate, and that will trigger, depending on what you return here, will allow you to handle the session or not. If you return false, it will be as if the view doesn't appear to the drop session, and nothing will happen. If you return true or do not implement this, will continue and call sessionDidEnter to indicate that the drag has entered your view.
User then moves their finger around, and will call sessionDidUpdate repeatedly, and you have to return a drop proposal here. Now, keep in mind, this will be called a lot of times, so try to do at least minimal work here. Don't do too much, because your frame rate will suffer and users don't like that.
When a user lifts their finger, will execute a drop. More about that later. And finally, will call sessionDidEnd to indicate that the session has ended and your interaction, by default, is ready to accept new sessions again. Now, let's pretend that the user did not lift their finger, and bring them back.
If they move outside, we'll call sessionDidExit to indicate that the session has left your view. It does not mean that the session has ended. It's still going on, so if the user lifts their finger outside of your view, we'll call sessionDidEnd again to indicate that the session has actually ended.
Now, let's pretend again that the user did not lift their finger, and bring it back, outside of the view, they bring it back in again, we'll call sessionDidEnter again, and start calling sessionDidUpdate to update the session and get a drop proposal. Now, pretend that the user has rested on a place inside your view where you cannot accept a drop. You'll return an operation cancel or forbidden. If the user then lifts their finger, we'll call sessionDidEnd right away. Nothing happens. No drop is executed, and we're just canceling the drop.
Let's focus on this drop proposal for a second. I'm not going to talk about the drop operations that was covered yesterday in the introductory talk, but there's two more properties that might be interesting. First, precision mode. If you set precision mode by setting isPrecise to true, your will hit tests inside your view slightly above the touch of the user.
So, the actual hit test location inside your view will be not under the finger, but slightly above. This allows more precise dropping inside your view, because the user can actually see where they are dropping. A good example is the Text controls. They use precision mode to show with carets where the user will actually drop the items inside a Text view.
You can see it here, that the caret is shown slightly above the touch where the user is touching the glass. If you would not do this, the caret will be below the finger, and it will be very hard to precisely drop inside a specific point in the text. So, if you do implement precision mode, please indicate some UI at the drop site to indicate to the user where they will be dropping this items.
Next up is prefersFullSizePreview. This brings us to preview scaling. As you might have noticed by playing around with iOS 11, if you start to drag something, it will scale it down. The system will always scale things down. Why do we do this? Because it doesn't make a lot of sense to have a big preview covering the screen and your UI, because it's interactive. If you blocked the screen with a preview that's too large, it's hard to navigate around, so we scale those down, but in certain cases, it might be interesting to prefer a full-size preview.
For example, you have a list and this list you can reorder. So, you pick something up and try to drag it up. It would not make sense to scale that whole item down, so you can add prefersFullSizePreviews here. There's two ways to do this. At the drag site, there's drag interaction prefersFullSizePreviewsFor session. Return true here, and we'll try to keep those previews full size, and at the drop site, you can set the flag to true on the drop proposal.
Note that this is a preference. You can ask to scale, not to scale, but we might not always honor it. There's certain conditions where the system will scale down anyway. A few of these are flocks. So, if you add more items to the drag, we will always scale those items down, even if you prefer full-size scaling.
A single preview, if you are dragging one item and dragging it outside your app, we will always scale that down, too. And finally, once something is scaled down, we will never scale it back up again. So, keep that in mind. It's a preference, but not something set in stone.
Let's go to performing a drop. When you're ready to perform a drop, user lift their finger, and we'll have to start loading the data at this point. In fact, this is the only moment in time where you can actually request data and allow it to succeed, because in any other of the lifetime calls, if you try this, it will always fail. Only in performDrop you have a chance of getting data.
There's cooperation required on the other side, so that's why I say, "There's only a chance," but usually you will get some data. These data loads are always asynchronous, so please don't block here. If you block for too long and you don't know how long this data will be taking to arrive there, will kill your app, and that's not the best user experience for our users. So, don't do this. Load data in the background will animate the items down into your view so the user can see you dropped, and then finally, we'll call concludeDrop to indicate that the animation is done, and as far as the user is concerned, the drop is finished.
This does not mean that the data is there yet. If you can see, the first call here is still going on. But, more about that later. How do you load data? There's a very useful call on the session called loadObjects of class completion. It's very good to load homogeneous data.
If you have only images in the drag or in the drop, and you know you can only accept those, use URImage as class here, and it will give you back a nice array sorted exactly the same as the sessions, in the sessions items array, and we'll give it right to you. We'll do the heaving lifting behind your back, and you'll get a nice array back. This completion block will be called on the main queue, so you can update URI right away.
If you have more mixed data here, or you wanted some more control, you can just iterate over the session items and load each of them individually, if you want. Use loadObject, or loadDataRepresentation, or loadFileRepresentation on the item provider. It gives you more fine-grained control over what you want to load and how, and it even allows you to load multiple file representations for each item, if you choose to. Keep in mind that this completion block will be called on the background queue, so if you want to do URI work here, dispatch to the main queue. I'm going to hand it over back to Wenson to show off how that actually works in practice.
[ Applause ]
Alright. Thanks, Tom. So now, I'd like to introduce the second part of our demo. This is the, this is the second demo app, called Drop Destination, and what we'll be doing here is building a photo gallery very similar to the Photos app, where dropping images will populate this area with additional Image views. So, the idea is that this flow should work. I should be able to drop here, and I should see more Image views. Now, of course, that didn't happen, so let's go into the code and see why that's the case.
So, this is where most of our logic is going to live, Droppable Image Preview Controller, and here, you can see that all we've implemented is sessionDidUpdate. So, it's no wonder that the drop doesn't work, because we haven't actually implemented any drop handling yet. I'm going to implement performDrop right here, and in this method, we are going to iterate through all of our items in the session. Now, for each item, if we are able to load a UI image, we're going to go ahead and insert a new Image view into our hierarchy and kick off a load from the itemProvider.
Now, when the itemProvider is done loading, we're going to call back to the main queue and set the image of the Image view that we just inserted to this new image returned by the itemProvider. So, with that little change, we should be able to get this flow, this basic flow to work.
So, let's see what happens. Now, the first thing you'll notice is that now there's a green plus-three badge. This indicates that there is indeed an action to be performed, and that action, of course, is inserting new images. So, that works. It's very basic, though. There is now another feature I'd like to highlight while we're here.
So, you might have noticed this area at the bottom that says Drop here to delete photos. It does what it says on the tin. When I drop it here, we remove it from the top area. So, that's kind of nice, but the thing is, we haven't done any customization around the drop preview yet, and so by default, images just kind of fly towards the center and fade out. I'm going to now hand it back to Tom to see what we can do to make this better.
[ Applause ]
Turns out, you don't need a lot of codes to perform a drop. So, let's talk about drop previews and their animations. Let's bring back this diagram, but it turns out that it's a bit more of a simplification, and there's more going on. So, let's bring this concludeDrop to the side, and let's talk about what's going on in between.
Started loading our data, and once we've performed our completes, we ask you for a preview for dropping the item by calling previewForDropping item with defaultPreview, giving you a default preview. You can return a new preview here, or return the default preview, or nil, whatever you want. More about that later. So, any of the previews we get, or the defaults we have, we'll use these and animate those down into your view, so the user can actually see something dropping.
While that's going on, we'll animate willDropWith animator so you can animate alongside. Now, as Robb mentioned before, same as in the lift side, the drop side is live, too. While you're dragging, there is a snapshot, but while you're dropping or canceling or lifting, the view is live, so you can also update the view you give us here, or animate alongside any other UI you have.
Those animations finish, and we'll call concludeDrop to indicate to you that the drop is finished and, as far as the user is concerned, they can continue with their business. Now, again, this does not mean that the data is already there. You can see here, and that's just two examples, there's one very long load object call that's going on beyond concludeDrop.
There's one that, like, ends in the middle between willAnimate and previewForDropping, and even the previews are animating at a different duration. That's because, depending on what target you give us, they might take longer to travel there. Something that's farther away from the finger will take a little bit longer than something that's closer to your finger. So, keep this in mind. The animations do not take the same time. They are slightly different.
So, we have drop previews, and as Wenson already showed, we also have cancel previews. They look almost exactly the same, and the same goes for lifting previews. It's the same approach, but just different locations. Wenson's previews demos showed that it's probably better to implement previewForCanceling item, because it gives a better user experience. You can fly back the items. When you update the UI, you can, I mean, the user can navigate around so your original UI can be very different than the one that you started at, so keep this in mind.
Additionally, willAnimateDropWith animator is very similar to willAnimateCancelWith animator. And again, different occasions, but the same approach. The UIDragAnimating protocol here is very similar to UIViewPropertyAnimator, so you'll be right at home, there. Now, let's talk about this default preview we give you. Why do we give it to you? You could just return it here, and you get this.
Well, that's fine, but that's not why we give it to you. So, if you do want the default preview, and you want the default animations, just return nil here. That indicates to the system that you're fine with the defaults and the system can do what it wants to animate everything down, and how it represents it.
So, why do we give you this default preview? Well, you can retarget it. That's why we, what we want you to do. If you retarget it, you know where it's going to be inside your view. We'll animate it down into the target you specified, and that's a better experience. That only works, of course, if you know where to target to. If you don't know the location, you can't retarget.
And finally, you can create your own custom preview and make your own UI here. You're free to do what you want. The preview you give us will animate to the target you specified. There's a few limits here. If there's fewer items in the flock, then we'll ask you a preview for each of the items and give you an alongside animation for each of the items, so depending on how much they are, you'll get these.
If there are many items in the flock, or in the session, then we'll use the default previews for all of them, so we won't ask you for preview. We do give you one alongside animation to go with that one animation for all the items. Don't take my word for it. Wenson's going to show how to do this.
[ Applause ]
Thanks again, Tom. So, to jog your memory, the part that we'd like to polish is this drop animation. Let's find out how to do that. So, we're now in Droppable Delete View, and over here, first thing we're going to do is implement a previewForDropping. So, given the item, we're going to create this drag preview target.
Now, this looks very similar to what we've done before, only this time, we have an explicit transform set, so what this is going to do is animate our default preview's width and height to 10% of its original value. So, you're going to specify that transform. We're also going to set the center to the be the iconView center. The iconView is, if you go back to the app, this little trash can at the very bottom, here, on the left.
So, we're going to animate to there, and we're going to retarget the default preview to that location using this target. But wait, there's more. We can actually do a little more polish here. Let's add an alongside animation on the drop, as well. So, let's add a transform to the iconView to the trash can as the drop is taking place. So, we're going to transform it to 1.25 scale. That's going to make it grow slightly, by 25%. Now, we don't want to have it permanently at 125% size, so we're going to set the transform back to the identity when we conclude the drop.
So, with these little tweaks, should be able to see a little more polished experience. I'm going to go back and drag some photos here. So, pay attention to what happens to the photo when I drop it. You see, this time it goes into the trash can and disappears. And, speaking of the trash can, you also see that kind of grow in size and then shrink when the drop is concluded. So, that looks a lot better than it did before. Now.
[ Applause ]
So, I'd like to now show you something that might not look as good. So, in this case, this is the last panel of Drag Source. We have Slow Draggable Image Views. Now, they're called slow because they're stimulating items coming in from a remote server far, far away. If I drop these four photos into here, it's going to take a really long time to load.
So long, in fact, that we will begin showing this app modal dialog that at least allows the user to cancel. But, as is the theme of this presentation, this can also be customized away. So, I'm going to hand it back to Tom to see how we can do that.
[ Applause ]
So, how do we deal with slow data delivery? Like I mentioned before, data loads are always asynchronous. So, there's two disconnected timelines at play here. There's data loading, one goal, and there's animating the drop previews, and they're not the same. Bring back this diagram, you can clearly see that there's not one line here that's equally in size. The loadObject calls take different time, and the preview animations take different time. And, you can also see that we don't have data yet at the moment we ask you for a preview.
One use case for this, or one case that you might run into if you drag photos from Photos into an email, and those photos might be stored on iCloud, because we're saving space on your device. While you're dropping, well show the app modal UI, giving the user some sense of progress and a way to cancel out.
That's a real-life use case. So, you saw this Cancel button, because we don't want to user to be waiting forever. We don't know how long the data will take to arrive. Might be two seconds, might be two minutes. So, we give the user a way to cancel. If that happens, we'll call the completion blocks with nil data and an error set so you can detect this. Additionally, both sessions and item providers provide ProgressReporting. The session is ProgressReporting, so you can observe its progress. And, the item provider load methods all return a progress object you can also use.
Progress has a cancellation handler which is a perfect spot to handle the cancel. Add you code there to handle any of the items coming in and not being there, and you can remove them again from your modal. Now, this also brings us to showing custom progress, like Wenson said before.
If you don't want this app modal UI, you can turn it off by setting session progressIndicatorStyle to none, and then we won't show the UI at all. Now, this does mean you have to provide that experience to the user, yourself. You can do this by observing the progress again. There's a progress on the session, and the per-item progress returned by the item providers.
If you do this, use this progress to indicate some UI there where the user is, where you can see, for each item, if it's loading or, in general, for your view, but please allow the user to cancel or navigate away, so they are not blocked on using your view.
But, the big question remains, how do I generate a preview if I don't have any data? I want to create this custom preview, but it doesn't work. Well, turns out, you can't. If there's no data, you cannot create a new one. Just use the default previews. They're a pretty accurate representation of what's actually in the item. Was set by the drag side, and so you can actually use this to animate this down. We target it, add a transform, you can change however you want it.
You can also make a placeholder progress view. If you have something that you want to show, and like, show a spinner there, that's probably a good idea, if it makes sense for your UI. One of the great things is that Collection view and Table view have built-in support for this, so you don't have to worry about it. It's very easy to turn on. So, I know it's early, tomorrow at 9 a.m., fourth day of WWDC, but please come to this session. It will be worth your time.
So, never assume the data will be there. That's the one advice I can give you. Even if you are testing, and locally it might be, the data might be there right away, this might not be the case for your users. You don't know how it's going to be in the field. And, always account for the worst case. That's the best approach you can take, even if it goes fast, does good, but assume that it's going to take a while. If implemented properly, this could look like this.
Custom preview, custom progress here, so your user is not blocked. Finally, let's talk about how to improve your in-app experience by adding drag and drop. Drag and drop is something you can use, to use between apps, but you can also use it to enhance your own app. There's a few nice things we added to accommodate that. First is localDragSession on UIDropSession.
This gives you access to the drag session, as I mentioned before. You can access any kind of data in the drag session. The items again, any stages that are, is available. It only works for in-drag apps. If you're dragging outside your app, the drop session will not have corresponding local drag session.
Additionally, as Robb showed before, there's localObject on UIDragItem. It's a very good container for local data. You can use it to have states, set some states in itemsForBeginning session, and use that state to generate a lift preview, for example. Or, you can use it to transfer data from the drag site to the drop site.
That's much easier than building itemProviders that will transfer your data outside your app. If you do allow the drag to go outside your app, you still have to do that, of course. And finally, there's localContext for UIDragSession which allows you to keep states for that drag session and the drop session, of course, locally, without resorting to app global states, and it makes things a bit easier for you.
How do you keep a drag inside your app? There's a method you have to implement on the dragInteraction delegate called sessionIsRestrictedTo DraggingApplication. If you return true here, then the drag won't be able to leave your app. The user will still be able to drag outside your app, of course, but any of the drop sessions outside your app will not see the drag. So, visually, it will look the same, but nothing will be able to accept it, only inside your app. You can also inspect this on the drag and the drop session, if you want to.
One last thing, local drag and drop for iPhone, this is disabled by default. On iPad, it's enabled by default. This is because you want your apps to behave according to size class so that if you have a side app, that works, too, but still, on the iPad, you can drag out into another app, but an app of the same size will not work on iPhone.
So, if you do go on to enable drag and drop inside your app also on the phone, you have to enable it by setting isEnabled on the drag interaction to true. That will enable the interaction, even on the phone. I'm going to hand it over to Wenson for a final demo.
[ Applause ]
Thanks again, Tom. So now, we're going to put together everything we've learned so far to implement our own custom progress UI. Let's just jump right into the code. So, we're going back and revisiting Droppable Image Grid View Controller. Now, here's the function that we implemented earlier for performDrop. We're just going to add a few lines here. It's going to look something like this.
First of all, we're going to set the progressIndicatorStyle to none. This is going to instruct UIKit to not show the app modal dialog. Next, we're going to remember a little bit of state for the drag item that is being dropped. Now, the important thing here is that we're going to remember the view that we are inserting into the view hierarchy of the grid, as well as this progress that's returned when we load object on the item provider. This is going to come in handy right now when we implement previewForDropping.
So, the first thing we do here is we're going to read some state out of our item states dictionary. This is going to describe information about the item that is being dropped. Namely, it will allow us to create a new progress spinner view. This is a custom view that just knows how to represent a spinner indicating the progress of a load. So, we'll create this custom view right here. The rest is very similar to what you've seen before. We'll create a target, create a drag preview using that target and parameters.
So, consider this. What happens if the image actually loads really fast? What will happen is, we'll show something at their destination while the drop is still animating, so we'll see this drop preview flying to the destination that already has content, and the drop preview is going to show this spinny loading progress. It's going to look kind of weird. So, this will handle that edge case right here. What we're going to do is set the alpha of our destination view to zero, so we're going to hide whatever we show at our destination while the drop is taking place.
Now, when the drop is finished animating, we're going to set the alpha back to one, and what's going to happen is that the drop preview at the destination will fade away and give way to show the actual destination view underneath, because we set the alpha to one this time.
So, that should take care of that. There's one last bit of bookkeeping we should do, and that's implementing concludeDrop. Now, when the drop is finished, we've just got to do a little bit of good bookkeeping and remove all of the items that are no longer relevant in our dictionary of item states. So, that was a lot of, those were a lot of changes. Let's see it in action.
So, I'm going to repeat the same scenario with Slow Draggable Image Views. Watch what happens. This time, we get a different progress UI for each drop that is happening, each item that is being dropped, and that is really cool, because it allows us to do things such as this. If I repeat the same procedure, you can see that I'm able to do things like scroll the Image view, sorry, the Grid view, and also interact with different items while the load is happening.
[ Applause ]
So, that's some really powerful stuff, and we've come a long way in today's session. I would like to now hand it back to Tom to give a quick recap. I'll see you at the labs.
[ Applause ]
That looks pretty sweet, if you ask me. Anyway, we talked about how drag and drop can be a very powerful and user-driven input-output mechanism for your app. You can create custom and very stunning visuals on the lift side, on the drop side, and when you're canceling. You can animate a lot. We talked about how to handle asynchronous data, and slow-running data. And finally, we mentioned how you can use drag and drop even inside your app, to make your app a lot better.
There is some more information here. You can re-watch this video, if you weren't here. That would be strange, I guess. There's a few related sessions, if you missed Introducing Drag and Drop yesterday, please watch the video, it's chock full of information. Again, there's two sessions tomorrow, on Collection view and Table view, and then data delivery, back to back, in Hall 2. Thanks for listening. Enjoy the rest of your WWDC, and see you around.
[ Applause ]