WWDC2017 Session 213

Transcript

[ Background Conversation ]
>> 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 ]