WWDC2013 Session 217

Transcript

X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
[ Silence ]
[ Applause ]
>> Good morning.
Thanks for coming out for
another UIScrollView session
this year.
For those of you keeping track,
this is actually our fifth
UIScrollView session in a row.
[applause].
So we've talked about a lot
of great stuff over the years.
And-- So some of you
might be thinking,
"What could they
possibly have left to talk
about with ScrollView
this year?"
But, rest assured,
that ScrollViews are an
incredibly versatile class
and there's a lot of
stuff left to discuss
that you can do with this stuff.
So, if you're familiar with
the structure that viewed
in previous years, you
probably know that we're going
to spend some time going
pretty deep into a couple
of specific examples of things
that you can do with
ScrollViews.
And we're going to do that
again this year and we'll get
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to that in just a minute.
But before we do, I want
to do something a little bit
different this year and kind
of take a step back and look
through iOS 7's new interfaces
and see how we're using
UIScrollViews in some new ways
that you might not necessarily
have expected we'd be using
UIScrollViews for so you
can get some different ideas
about how you can use this
kind of stuff in your own apps
to create some interesting
different effects.
So let's get started.
The first thing that you see
anytime you start on your phone,
of course, is the Lock Screen.
And starting in iOS 7, the
first time that you turn
on your phone, every time, the
first that you'll do is interact
with the UIScrollView.
So, kind of big deal.
The Lock Screen is actually now
Slide to Unlock is implemented
as a two-paged paging
ScrollView.
So when we swipe over here
to the Passcode screen,
we're just paging
between two pages.
It's easy to imagine
how that's put together.
Over here on the right hand
side, we've got the first page
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and we're just swiping back
and forth between them.
Now, even before we get into
unlocking, there's more uses
of UIScrollView defined right
here on the Lock Screen.
So of course, if I get
a notification saying
that I have a WWDC presentation
right now in Presidio,
that is itself in a
vertical scrolling list
because you might have multiple
notifications to scroll through.
Then that's embedded within
that outer paging ScrollView.
It's an embedded
vertical ScrollView.
Now there's actually even
more because each one
of those notifications is
itself embedded inside another
horizontal paging ScrollView.
So you might wonder why that is
and it's actually pretty easy
to see once we start
interacting with it.
When you swipe on an
individual notification,
that will take you
directly to the application
that generated that
notification.
So if we start interacting
with this one,
you'll find that we can drag
it a little bit on its own,
but eventually it'll
catch the outer ScrollView
and start pulling
it along with it.
And then if we let
go, it just comes back
to rest at the beginning.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
If we were to finish
that gesture,
we would be scrolling
the inner ScrollView
for a bit then we would catch
the outer one and pull both
of them over on to
the Passcode screen.
So this is a pretty interesting
effect being generated
by multiple Nested ScrollViews.
And this is actually the first
technique that we're going
to look at today and
go into a lot of depth
on how exactly that
was implemented.
So, we'll show you the
construction of the views
and see how we make those
ScrollViews interact to get
that behavior where
one pulls the other.
But before we get to
that, we're going to look
through a few more examples in
iOS 7 of this kind of thing.
So let's unlock the
phone and take a look
at some more examples
of Nesting ScrollViews.
So right here on the home
screen, I'm sure as you remember
in the past, we used
to have spotlight
over on the left hand page
of our paging ScrollView.
In iOS 7, it's moved up to
the top and that's implemented
as a Nested ScrollView inside
the outer paging ScrollView.
Moving into the multitasking
user interface, we can now find
that the way that we kill
applications in iOS 7-- oops.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
The way that we kill running
applications in iOS7 is
by taking this interface and
scrolling it and flicking it
up off the top of the screen.
That is also now a
Nested UIScrollView.
Now, the interesting thing about
these Nested ScrollViews is
that we get a really
consistent experience throughout
the interface.
Every different place
that we see this kind
of interaction feels the same
way, has the same bounce,
has the same deceleration.
It really just makes everything
feel natural and consistent
across the entire
operating system.
So if you're looking at
doing any of these kinds
of interactions, I
would encourage you
to consider using a
UIScrollView to build it
because you'll get the
same feel that you have
across all the other
parts of iOS 7.
So Nesting ScrollViews,
that's a pretty common one.
Another thing that we found
has cropped up in a number
of different apps on iOS 7 is
multiple ScrollViews that scroll
through the same content
at different rates.
So again here in
the multitasking UI,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
you can see that there are
actually two independent
sections of content.
Up at the top, we've got
the individual snapshot
of the applications and we
can scroll through those.
And down at the bottom,
we have their icons.
Now, as we scroll
through the top part,
we'll move fairly slowly
because we're moving
through those images.
But you'll notice that at the
bottom, the icons kind of group
up and now we see
more of them at once.
What that allows us to do
is scroll at the bottom
at a faster rate to get
through things more quickly.
So, two different
ScrollViews scrolling
through the same content
at different rates.
Another place where
we find that kind
of thing implemented is here
in the Calendar application.
So we can page through the
paging ScrollView at the bottom
which represents that
days of the week.
And as we do that, we
go one day at a time.
We can see all the days
and go nice and slow.
If we want to go faster,
we can scroll up at the top
where those numbers are,
and that would let us,
with one gesture, move
an entire week back
with what otherwise had taken
us seven swipes to get through.
So this idea of multiple
ScrollViews scrolling
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
through the same content at
different rates is showing
up in a number of
different places.
It might be something that you
might want to consider putting
into your own applications
if there's appropriate
places for that.
Another thing I want
to take a look
at here is some custom
content transforms
that are being applied
to different pieces
of content inside UIScrollViews.
So, the first one that I'd
like to take a look at is here
in the Clock application.
What you'll find is that there's
the 3D Transform being applied
to the numbers as we scroll
through on the right
hand side there.
This has always looked
vaguely 3D.
But in iOS 7, this is actually a
3D wheel being constructed using
3D Transforms on
[inaudible] layers.
Now, this is still the exact
same scrolling behaviors
that you expect across the
operating system, the same feel
of movement, same deceleration,
it's just that there's
some transforms applied
to the content to give an
interesting visual effect
that makes it unique in
this particular location.
Another example of this kind of
thing can be found in Safari.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
If we go into the new Tab
Switcher interface in Safari,
you'll find that as you
scroll, some of the tabs
at the top start to pull out
of the screen towards you.
Now again, this is
just a UIScrollView
but we're applying Transforms
on the content while we scroll
to give this other interesting
visual effect that's unique
to this application.
One last place where we see this
kind of technique being used is
in the Passbook application.
As we scroll within passes,
they're bunching
up near the top.
And then as we pull down and
it rubber bands, they stretch
out from one another
a little bit.
Again, just a UIScrollView,
we have the exact same feel
of the bouncing, but we get an
interesting visual effect unique
to the Passbook app.
So, all of these techniques
are fairly easy to implement
by just applying
different Transforms
to the content while you're
scrolling in your UIScrollView.
Now the last thing I want to
take a look at before we get
into the specific examples
of implementing some
of this stuff is the
Messages application.
I'm sure you've noticed as
you'd been using Messages
that when you go into a
conversation and you scroll
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
around it, the Chat
Bubbles have a little bit
of a bounce to them.
It gives it this really lively
feel as you're scrolling
around in this application and
it's really unique compared
to what you see in
other apps on iOS 7.
Now this is actually the second
thing that we're going to talk
about today and see
exactly how this kind
of thing can be implemented.
It's actually become
really easy to do in iOS 7
with the introduction of
the new UIKit Dynamics API.
So, we're going to get into
that just after we talk
through the first
Lock Screen example.
So, let's get into building
some of these things.
And as I mentioned, the
first thing we want to talk
about is the Lock Screen,
those Nested UIScrollViews.
So let's take a look at that
again just for a quick reminder
of how this is going
to be put together.
So we've got here
our Lock Screen.
On the left side, we've
got another page of content
which is the Passcode lock.
And of course, this is a paging
ScrollView with two pages
and our content size
is represented
by the width of those two pages.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Now, that ScrollView is actually
a full screen UIScrollView
because it's going
to be scrolling all
of the content that's
visible on the screen.
Nested within that, we
have the notifications.
And that's a much
smaller UIScrollView
and it scrolls only vertically,
but it's here as a child
of that outer paging ScrollView.
And then as I mentioned,
each one of these is also its
own nested horizontal ScrollView
and they're a little bit smaller
because they're just
scrolling the individual
notification itself.
So we've got one
here for the top
and then another for the bottom.
Now as the user starts to
move their finger on this,
the content in that will
get pulled over to the point
where it's going to catch
that outer ScrollView.
As they finish their
gesture and finish scrolling
that inner page, it pulls
the outer one along with it
and gets us over
onto the Lock Screen.
Now, if you've used UIScrollView
before in your apps,
I'm sure you're familiar
with the fact
that ScrollViews will not
automatically start dragging
their outer containing
ScrollView
if you just nest
one inside another.
So something must've
been done in order
to make this behavior happen.
So let's look at how
we would do that.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Well, where might we want to
implement this kind of thing?
If you've watched previous
sessions and seen some
of the techniques we've used for
implementing custom interactions
in ScrollViews, you
probably already have an idea
of what I'm going to say.
And that, of course, is that
there's the delegate method,
scrollViewDidScroll,
which allows us to find
out every time a ScrollView's
contentOffset changes
and do something
in response to it.
So in this case, the
thing that we want to do
in response is have the
inner ScrollView tell us
when it scrolls and then move
the outer one along with it.
So what's that going
to look like?
Well, we're going to implement
our scrollViewDidScroll
delegate method.
We're going to find out how far
the inner one has scrolled pass
the point where we wanted
it to catch the outer one.
We want to give some free
scrolling before it grabs the
outer one, so there's
some initial movement
that's required.
So let's find out how
far we got beyond that.
Once we have that value,
we're just going to apply it
as a new offset onto
the outer ScrollView.
So we'll take that
outer ScrollView
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and shift its contentOffset by
the delta we just calculated.
So, actually pretty
straightforward.
And to show us exactly how
we can put that together,
Eliza is going to come up and
start doing a demo for us.
[ Applause ]
>> Hi, I'm Eliza
and I'm an engineer
on the Springboard team.
So what I've got here
is an application
that I built entirely--
well almost entirely
using Interface Builder.
It doesn't do very much yet,
but it illustrates the hierarchy
of ScrollViews that
Josh already described.
So we have an outer paging
ScrollView that scrolls
between this list of
colors and the building.
I'm reusing the building
views that I wrote
for our session two years ago.
So, we can scroll back and
forth between those pages.
Now, the first page of this
two-paged paging ScrollView
contains a vertically
scrolling collection view.
And then each individual cell
in the collection view contains
a horizontally scrolling
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
ScrollView that scrolls
the color for that cell
on and off the screen.
So, this is set up with
remarkably little code.
And what I want to do
now is to jump straight
to showing you how we can add
the behavior that we're getting
in the Lock Screen where
as you start scrolling one
of these cells, it scrolls a
little of the way over, catches,
and then pulls the outer
paging ScrollView with it.
So, of course, this isn't
happening by itself.
And in order to do this,
we're going to need
to make a mechanism that the
inner ScrollViews can use
to communicate with the View
Controller that's managing the
outer paging ScrollView.
And the way that I'm going to
do that is a delegate protocol.
So here in my scrolling
cell, which is a subclass
of UICollectionView cell,
I'm going to add a scrolling
cell delegate protocol
that our View Controller
is going to conform to so
that it can find out when
the inner cell has started
to scroll.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So, we're going to need a
couple of methods in here.
We're going to need
a ScrollView--
scrolling cell did begin
pulling, so that's going
to get called when you
get your finger passed
to that catch point and start
pulling the outer guy with you.
We need to find out
at every frame
of scrolling how much
the pull offset changed.
So every time that the
pull offset changes,
the second method
will get invoked.
And finally, we need to be
able to tell our delegate
when the scrolling
cell stops pulling
so that the delegate
can clean up any state
that it might have put
into effect once the
scrolling started.
OK, so we've got this
delegate protocol.
We need to make it possible
for a delegate to be set
on our scrolling cell and then
in order to make Xcode happy,
we need to pre-declare
our scrolling cell
delegate protocol.
OK, so now that we've-- I've
described what we're going
to build, before I
go and show you how
to implement this scrolling
cell side of things,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
I'm going to head over
to the View Controller
that is managing the
outer paging ScrollView
and show how we can actually
adopt this delegate protocol
because that's actually going to
turn out to be very little code.
So this is my View
Controller class.
Pretty much, all it does
so far is be a UICollectionView
data source
and return these
scrolling cells.
So, we need to make this
View Controller conform
to the scrolling cell
delegate protocol.
When we make a scrolling cell,
we need to set its
delegate to our self.
I'm going to just adjust the
number of cells here to make it
so we've got more
to scroll through.
All right, now we
need to go ahead
and implement these
three delegate methods.
What we're going to do is
when we find out that one
of our little scrolling
cells started pulling,
we're going to programmatically
adjust the contentOffset
of the outer paging ScrollView.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And because we're going to--
we're basically giving the
inner ScrollView control
over the outer ScrollView's
contentOffset,
it turns out to be sort of
awkward if we also continue
to allow the user to interact
with the paging ScrollView
while that's happening.
So when the scrolling cell
begins pulling, what I'm going
to do is I'm going to tell
the outer ScrollView not
to allow user scrolling.
And then when the
scrolling cell stops pulling,
I'm going to re-enable
user scrolling
on the outer ScrollView.
When the scrolling cell
changes its pull offset then,
I'm simply going
to programmatically
set the contentOffset
of my outer ScrollView
to the correct--
well, to the offset
that was reported to me.
And then just to make it
just a little bit more fun,
when we start pulling
one of the colors over,
let's have the building
change its color.
So we'll draw a new building
with the color of the cell.
All right.
So that's pretty much
all we need here.
So I'm going to switch back
to the scrolling
cell implementation
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and we can see how to go
about calling these methods
to cause the scrolling
to take place.
So the first thing we need to
decide is how far are you going
to get in your scrolling of one
of these inner ScrollViews
before you start pulling the
outer ScrollView with you.
And I'm going to do that
by simply defining a
pull threshold.
I've chosen 60, but
obviously any number
within reason would work.
We're also going to need to
keep track of some state,
are we currently pulling
the outer ScrollView
because we're going to-- we
need to tell the delegate
when this starts and stops.
So with that in place,
we can then go ahead
and implement the
UIScrollView delegate protocol.
And in particular, we're going
to use the scrollViewDidScroll
method to find out every time
that our inner ScrollView's
contentOffset changes.
So, we can get the current
contentOffset which is,
since we only care about the
horizontal direction here,
it's just the ScrollView's
contentOffset.x. Now let's
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
figure out whether this offset
represents the beginning
of pulling.
So did we just start pulling?
We just started pulling
if the offset got bigger
than the pull threshold and
we weren't already pulling.
And if that's the case, then
we can tell our delegate
that the ScrollView-- the
scrolling cell began pulling
and we can set our flag to Yes.
All right.
So if we are pulling,
we now need to tell the
delegate an additional thing.
We need to tell the delegate how
much we changed the pull offset.
So, we'll calculate what
the new pull offset is
by subtracting the
pull threshold
from our current
internal offset.
And what I've written here
is actually not quite right.
So imagine the following
scenario.
I start scrolling in this
inner cell, I get passed
to the pull threshold
so I'd start pulling,
and then I move my finger
back and forth a little bit,
and I get under and
then over and then
under the pull threshold.
We don't want to
report a negative number
to our outer paging ScrollView.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
That would have some
weird effect.
So what we want to do
instead is make sure
that we always report
a minimum of zero.
So, we're going to take the
maximum of zero and the offset
that we just calculated.
And then we can tell
the delegate
that the scrolling cell changed
the pull offset to that amount.
The last thing that we
need to do is to figure
out when the pulling stops.
And what I've decided to
do here is just decide is
that the pulling is going to
stop when the scrolling stops.
So, we need to figure out
when the scrolling ends.
And if it-- when it ends,
we can tell our delegate
that the scrolling
cell stopped pulling
and we can set our
flag back to No.
All right, how are
we going to figure
out that the scrolling ended?
If you've seen any
of our sessions,
I feel like I write
this code every year.
There's two different ways that
scrolling can come to stop.
One is that you're
scrolling, you have no momentum
and you lift your finger,
and then the scrolling
ends right then and there.
Alternatively, you
may have some momentum
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
when you lift your
finger, at which point,
there's a deceleration
period first.
So we need to catch both of
those cases and we can do
that with two further
delegate methods,
scrolling cell
scrollViewDidEndDragging
:willDecelerate.
And if we're not decelerating,
then that means the
scrolling ended.
Otherwise, we're going to
catch the scrolling ended
when the deceleration ends.
All right, so with
all of that in place,
I'm going to go head
and run this.
We have more cells,
which is nice,
which we can scroll through.
OK, so now if I grab one,
it scrolls, it catches,
and it starts pulling the
outer ScrollView with us
which is exactly what we wanted
and we can get over
to the building.
So now, there's a couple
of bugs here that I want
to draw your attention
to if your attention has not
already been drawn to them.
One is that we obviously
didn't get far enough over,
so we're sort of stuck on
a weird page that's like in
between page boundaries with our
building view not all the way
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
in the picture.
Another problem is that
our cell is missing,
the one that we just
scrolled over is gone.
And then another problem which
is a little bit less obvious,
although maybe it was obvious.
Here, my finger is
where the arrow is
and I start dragging
and I catch.
And now, I would expect that
green cell to stay pinned
under my finger as it
drags the stuff with,
but that's not what happens.
Instead, it zooms out from under
my finger and then disappears.
So I'm going to, in a minute,
turn things back over to Josh
to explain two of
these problems.
But the problem where
the cells are just gone,
that's actually really
easy to fix, so I'm going
to just fix that right now.
That's happening because
we're scrolling the content
of those cells off the screen
when we pull the
outer ScrollView over.
So we can fix that
by simply, you know,
our scrolling ended method here
just setting the contentOffset
of our ScrollView back to zero.
But now for the other two
bugs, the zooming out from
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
under your finger
bug and the fact
that we're not getting all the
way over to the building page,
I'm going to send it
back over to Josh.
[ Applause ]
>> All right, thanks Eliza.
So, we're getting pretty close.
It's almost there.
Just a couple of
bugs to take care of,
nothing we can't handle.
So, let's take a look at how
we ended up with only part
of that page visible first.
Now, we have two pages
worth of content.
So that gives us a content
size represented here
by this yellow square,
it's about two pages.
And we've defined the size
of our inner ScrollView's
content to be equal to that.
So we've got that nested
inner paging ScrollView
and it's also got
two pages of content.
That's how we're going to
cause it to get pulled.
So let's see what's going wrong
as our user starts to drag here.
We start pulling that inner
ScrollView a little bit
and it moves over to the right.
But now you'll notice that
the sizes of our content
which started out aligned on
the left have now pulled away
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
from each other 'cause the
inner one has moved a bit
but the outer one
hasn't started yet.
So now, if we let the
user finish the gesture
on that inner ScrollView, it's
going to finish paging over
and come to rest, and it will
hit its end before the full
content size of the outer
ScrollView has been pulled
into view.
So it's really the case that
that inner ScrollView needs
to scroll farther than the outer
ScrollView because it's going
to move a little bit by
itself before it catches
and starts pulling
the outer one with it.
So how do we make the inner
ScrollView scroll more?
Well, as you know, the way
that a UIScrollView's
content size is defined is
by its bounce-- I'm
sorry, of the paging
with is defined by its bounce.
And in this case,
we need two pages
of content 'cause we have a
two-paged paging ScrollView.
So we need a larger bounce in
order to get a larger page size
so we have more content
to scroll.
So, if we look at the bounce
of our ScrollView right,
it's the exact same width
as it its containers.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So that gives it
the same page size.
To get a bigger page size,
we have to make it wider.
Now, let's find here.
We can let it hang off the
right side of the screen
because it's not drawing
anything over there.
We're just making it bigger so
that it can scroll more content.
And then you'll notice that
we are still using two pages,
so we've doubled our content
size and that causes it
to hang off on the left as well.
So let's see what happens
if we start scrolling
with that configuration.
Now, we start pulling that
inner one in a little bit.
And at the point where it's
going to catch, the left sides
of our content size of both
ScrollViews are now aligned.
Now, that seems a lot better
because if we let our
ScrollView finish pulling,
it's going to pull
that outer one with it,
and they'll both come to
rest when they have all
of their content fully visible.
So just making that
ScrollView a little wider
to give us a wider page
and more room to move
on the inner ScrollView,
fixes that first problem.
But now something else is
still wrong because we had
that second problem that Eliza
showed you where we ended
up having the content shooting
out from under her finger
as she was scrolling
the inner ScrollView.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So, why was that happening?
Well, to understand
what was going there,
it helps a little bit more
to look again at the frame
and bounce of the
inner ScrollView.
So here, we have
it visible right
in the middle of our screen.
Let's take a look step by
step at what's going on again.
We start pulling a little
bit with our finger.
We get to the point where
we're going to catch.
Now if we hadn't anything else,
if we weren't doing the bit
where we're pulling
the outer ScrollView,
what would've just
happened is we'd keep moving
that inner ScrollView's
content along.
But of course, what we are doing
then is calling setContentOffset
on the outer one to
cause to move with it.
So what we think that should be
doing is pulling the ScrollView
with it.
Unfortunately, what we
forgot about here is
that that inner ScrollView
is actually a subview
of the outer ScrollView.
So not only does the
outer ScrollView move,
but it also ends up
pulling the inner one along
because it's a child of it.
So, we're-- the inner
ScrollView is getting scrolled
by its own gesture recognizer
and then as a result of that,
it's telling the
outer ScrollView
to move which moves it again.
And the really bad part here is
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that the red area representing
our frame is now moving
off screen.
So, the ScrollView
is actually moving
out from under our finger.
And this is causing the
double scrolling Eliza saw
which is pulling all
the content too fast.
So we really want to
pull that ScrollView back
so the left sides of the
content remain aligned
and the ScrollView,
the inner one,
remains centered on the screen.
Now this is actually very
similar to a technique
that we should a couple of years
ago in 2011 for pinning views
in place while you're
scrolling a ScrollView.
This is the same kind
of technique that's used
in UITableView, when
you scroll vertically,
you get a table header and it
pins to the top of the screen
as content scrolls under it.
We want to do the same
kind of thing here.
We want to pin that inner
ScrollView to the left side
of the screen so it stays
in place even though its
parent is moving under it.
That way, when the user moves
their finger more and lifts,
this outer ScrollView will
come to restfully on screen
and the inner one which is the
child of it will still remain
in place on screen and
everything will work right.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So what's that going to
look like in our code
that we already wrote?
Well, it's actually just
a really small change
to what we've already got, just
one little thing that we have
to add at the end here.
And that's that we need
to translate the child
by the same amount that
we're pulling the parent.
So we want to undo that
movement that we're adding.
We're going to pull
the parent some amount
and then translate the child
back by the same amount
so it doesn't double pull.
So Eliza's going to come
back now and fix our demo
so that this all works.
[ Applause ]
>> OK. So, we have
two problems to fix.
The first one, if you recall,
was that our page wasn't getting
all the way scrolled over,
so I'm going to address
that one first.
We need to go down to the bottom
of this file here in a part
that I didn't show before
which is where we're laying
out the subviews of our inner--
internal to the cell ScrollView.
So you'll see that I had
calculated a page width
to be the size of
my cell's bounce
that was causing the problem,
wasn't wide enough given
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that we have this pull
threshold to contend with.
So, what I'm going to simply
do is add the pull threshold
to the page width.
And then because I've
actually determined the frame
and the content size of
the ScrollView in terms
of the page width, everything
should then just work
from that point.
So now to address the issue
of the shooting out from
under your finger problem
and just scroll back up here
to my scrollViewDidscroll
method.
When we tell the delegate
that our pull offset changed,
the delegate is going
to turn around
and change the contentOffset
of the outer ScrollView
which contains us.
And so, that's causing us to get
pulled along with it resulting
in the double scrolling.
So what we'll do to
counteract that is
to simply set a Transform on
the ScrollView which is one
of our subviews and we'll set
a Transform which simply uses
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that very same pull offset
so that the ScrollView scrolls
along with the outer ScrollView
and as a result, sort of
surprisingly, it actually seems
to remain in the same
place on the screen.
This is a technique that
we, as just Josh mentioned,
talked about in 2011 with
getting a moon to stay fixed
on the screen when
you're scrolling a bunch
of buildings out from under it.
So, when we finished
scrolling here
and set our contentOffset
back to zero,
let's also set the
Transform back to Identity
so that we don't end up in a
weird transformed state while
we're not doing any scrolling.
OK. So with that in place,
I'm going to build again.
And I can grab one of
these cells, it catches
and it stays fixed
as we want it.
And it gets all the way over
to the building this time.
So this looks like it's
working pretty well.
It's doing pretty
much what we want.
And notice that the buildings
change color as planned.
So, there's one bug here that
may not be immediately obvious
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and I'm going to do something
to make it a little
bit more obvious.
I'm going to change
the pull threshold
to a larger number
and run it again.
So if I scroll this cell over,
now I can get it a little
further before it catches
and then I let go.
Watch really carefully, when
I let go so that this going
to return back to
the zero position,
you'll see that the outer
paging ScrollView kind
of slams into place.
The inner ScrollView comes
to a nice decelerated rest
as you'd expect from
a ScrollView.
But the outer ScrollView,
it's pretty abrupt that way
that it hits the
edge of the screen.
So, this is actually
kind of what you'd expect
to happen given the way
that this is implemented.
When I move this back and forth,
you can see that the inner
and the outer ScrollViews are
moving at exactly the same rate.
They're sort of pinned together
with 120 points difference.
So when this starts
moving back, of course,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
while the inner ScrollView
is still decelerating,
the outer ScrollView
is going to hit zero
and then the inner
ScrollView continues.
What would be nice would be
is if we're in this condition
where we're scrolling
back to zero,
if we could change
the rate of scrolling
of the outer ScrollView so that
both ScrollViews would come
to rest at the same time.
And I'm going to show
you how to do that.
It's a technique similar to
the techniques that we're using
in other places in iOS 7
to get these two ScrollViews
moving at different rates.
So the first thing that we need
to do to accomplish this is
to keep track of whether we're
in this decelerating back
to zero condition,
'cause we're going
to do something special
in that case.
What we're going to do in
that case is move the outer
ScrollView at-- with--
at a fraction of the speed
of the inner ScrollView.
So we need to figure out
what that fraction should be.
And I'm going to do that by
storing off a deceleration
distance ratio.
We want the ratio of how
far the outer ScrollView has
to move compared to how far the
inner ScrollView has to move,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and then we're going to
slow down the scrolling
of the outer ScrollView
by that proportion.
OK, so how can we detect that
we're decelerating back to zero?
We can do this by implementing
an additional ScrollView
delegate method,
scrollView WillEndDragging:
withVelocity:target
ContentOffset.
So, you may have seen
this method in the past.
It's often used to change the
landing position of a ScrollView
when it starts decelerating.
We're going to use it not for
that purpose but simply to find
out what the landing
position is.
Because if the landing
position is zero,
so if the target
contentOffset is zero
and our current contentOffset
isn't already zero,
it means that we're about to
start decelerating back to zero
which is what we
wanted to find out.
So, we'll grab our
current offset.
And then if the target
contentOffset.x is zero
but my current offset
is greater than zero,
that means that I am indeed
now decelerating back to zero.
And then, of course,
when the scrolling ends,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
I just need to remember
to clear that flag.
We're not decelerating back
to zero once we've
stopped scrolling.
OK, so now, if we are
decelerating back to zero,
we need to figure out the
ratio of the distances
that are going to
be scrolled here.
So we already have
our own offset.
We can calculate the outer
offset which is the pull offset,
the same way that
we did it above.
And then our deceleration
distance ratio is simply the
pull offset divided by the
inner ScrollView's offset.
So now we can use that
up here in this code here
where we were calculating what
to report to the delegate.
So we were reporting that the
delegate's pull offset should
always be basically
moving together with ours.
But instead, we're going to do
something different in the case
where we're decelerating
back to zero.
So first of all, if we are
not decelerating back to zero,
we're going to do the
same thing we did before.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
But if we are decelerating back
to zero, we're instead going
to choose a pull offset
which is our own offset times
that ratio that we calculated.
And that's going to cause
the outer paging ScrollView
to move more slowly when we're
in this decelerating
back to zero condition.
So I'm going to go
ahead and run it again.
And you can now see that if I
let go of this and let it return
to zero, they come in
for landing together.
So that's another technique
that's maybe useful.
And I'm going to turn
it back over to Josh
to talk about another topic.
[ Applause ]
>> OK, so that's
topic number one.
Now, as I mentioned, we
also want to take a look
at how we built the
Messages Interface
with those really
interesting bouncy bubbles
as you're scrolling
through conversations.
Now, this is actually a
really, really cool stuff.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And if you didn't have a chance
to watch the earlier UIKit
Dynamics talk this week,
I would strongly encourage
you to go check that out.
There's some very
cool stuff there.
This is not actually a UI
Dynamics session, so we're going
to go over a bit of stuff
here and give you an idea
of how you can use it
in this configuration,
but we're not going to go
into a huge amount of depth
on that particular API.
Similarly, we're going
to use a UICollectionView
in this demo here.
And if you haven't used
UICollectionView before,
there was a great
talk last year in 2012
about how UICollectionView
can be used,
and I'd encourage
you to go watch that.
We're going to talk about
some of the high level details
of how you use it here,
but we're not going to go
into a lot of depth again.
So the really interesting
thing that we're going
to show is the interesting
marriage of those two APIs,
CollectionView and UIDynamics,
and we'll see how they fit
together to make this kind
of interface really,
really easy to build.
So, let's take a look at what
it is that we want to build.
Some beautiful great squares.
But the interesting
thing about them is
that when our user scrolls them,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
we're going to have them
get this really nice
bouncing effect.
I've exaggerated it here so
it's more visible than it is
in the Messages application.
You might want to tone it down
a little bit so that you don't,
you know, cause your
users to go too crazy.
But we want to build something
that looks just like that.
Now, if we didn't do anything,
of course, what would happen
as we scroll this
kind of content is
that everything would
just move, stuck together,
and it feel pretty static.
It's, you know, it's the kind
of scrolling you expect to see.
But it's not quite
as interesting
as what we're going for.
So what do we have to change
in order to make it go
from that to the bounciness?
We have to have kind
of a conceptual shift
in what we're trying to do.
So by default, UIScrollViews
scroll its content area and all
of its children just move
along with it directly pinned
under the finger, moving
with the ScrollView.
Instead, we want to
behave as if each
of these individual cells
is pinned to the super view,
pinned to that ScrollView
at their center point.
And when we scroll,
we actually want
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to move the attachment points
instead of the views themselves.
We want the individual child
elements to remain in place
and resist scrolling and just
move their attachment points.
Then we can attach
the individual cells
to their attachment
points with springs
and let the dynamic
system pull them along
and bounce into place.
So how are we going to do that?
Well, you know, you
just heard me say,
the scrollViewDidScroll delegate
method for the last sample,
we talked about that
a lot in recent years,
so you wouldn't be too surprised
if I were to say that now.
But in this case, we're going
to do something a
little bit different.
We're going to go ahead and use
a UICollectionViewFlowLayout.
A UICollectionViewFlowLayout
is the object that you use
to represent locations
of things on the screen
within a UICollectionView.
So this is the point where
if you're not really familiar
with UICollectionView,
I strongly encourage you
to read a little
bit more about it.
We're going to talk about
the details that you need
to understand in order
to build this UI,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
but there's a lot
more depth here
that lets you do even
more powerful things
if you go a little bit further.
So once we've got our
UICollectionViewFlowLayout
subclass, we're going
to do a few things in it
to tie it together with
the UIDynamic systems.
So we're going to create
a UIDynamicAnimator
which is the main entry
point to UIKit Dynamics.
This is the thing that
represents the physics world
and lets us build
up the interface
to actually get those
bouncing effects.
Now, to create the attachments,
the springs that
we talked about,
we're going to create UIDynamic
behaviors, one for each item
in our collection view.
Now a UIDynamic--
UIAttachmentBehavior is a
particular type of behavior
that represents a spring,
and it's going to give us
that bouncing effect
that we're looking for.
So then, the only other thing
left to do in order to complete
that interface will be to
actually stretch those springs
out as the user scrolls.
And we'll do that by
stretching them out, obviously.
Now, as I mentioned, we're going
to create this
CollectionViewLayout subclass,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
so let's see what
that looks like.
And now there's three methods
that we have to implement.
We've got the prepareLayout
method,
and prepareLayout is called
by the collection view
to get your CollectionViewLayout
subclass ready
to display things on screen.
So in order to figure
out what to display
at particular locations,
you create these objects
called UICollectionView
LayoutAttributes.
A UICollectionView
LayoutAttributes object
represents the position
of an item
in the collection
view on the screen.
Now we're going to take
advantage of the fact
that we're a subclass of
UICollectionViewFlowLayout
to create these objects for us.
Our super class to, the flow
layout, already knows how
to do layout of the thing--
of things in a particular
grid-like structure,
so we can just call
through the super class
to get the initial positions
so we don't have to do any
of the math to do the
initial layout calculations.
That's really the benefit
of being a subclass
of the flow layout here.
It's going to save
us a lot of code
for doing the initial
location setup.
Then once we've got that,
we're going to create a
UIDynamicAnimator which is going
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to represent that physics world
and create all the
UIAttachmentBehaviors.
We're going to create
one UIAttachmentBehavior
for each UICollectionView
LayoutAttributes object
that we have because the layout
attributes object represents the
element on screen, its position,
and the attachment behavior
represents the spring attaching
it to its attachment
point in the ScrollView.
So we want one for
each paired together.
Now these other two methods
that we have here are going
to be really, really easy.
And so easy that I'm going to
write the code on the slide here
because it's just that simple.
A UIDynamicAnimator
and UICollectionViewFlowLayout
are designed
to work really well together.
So in this case, we've got
items for Rect as a method
on UIDynamicAnimator which
is exactly what we need
to answer the collection
view question,
layoutAttributes
ForElementInRect,
so we can just pass the
result directly back.
Additionally, we've got
this layoutAttributes
ForCellAtIndexPath, and
we can return the result
of that directly for this
other CollectionViewLayout
subclass method.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So with those two
things implemented,
that will give us
everything we need in order
to represent our
CollectionViewLayout
with the bouncing effects.
So then the last
bit to represent all
of this is stretching
the springs.
So how do we do that?
Well, as I mentioned, we've got
the scrollViewDidScroll delegate
method that we usually
use for this kind of thing
but we're not going to
do that this time around.
And to understand why, it helps
to remember a particular
property of UIScrollViews
and that is that the
ScrollView's contentOffset is
equal to its bounds.origin.
Those two are the same thing.
Now the reason that
that is important
in this particular example is
because there's actually a
method on UICollectionViewLayout
that we can take advantage
of to get the information
we're looking
for in the CollectionViewLayout
subclass itself.
And the reason we want
to do that instead
of using the delegate method is
because we're writing all
these codes in a subclass
of UICollectionViewFlowLayout,
but the flow layout is this
layout object that exists
to help a collection view
display content on screen.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
scrollViewDidScroll is a
scrollView delegate method.
The CollectionViewLayout
is almost certainly not the
ScrollView delegate.
So if we were to try
and use that method,
we'd have to create
a tight coupling
between the collection view
and the CollectionViewLayout
by passing that information
about scrolling through
and it would create
just too much structure
when we really want to
keep those thing separated.
So instead, we can
advantage of this method.
It's called shouldInvalidate
LayoutForBoundsChange.
Now, the nice thing here is that
we're passed in the new bounds
that we're changing too
and since the bounds.origin
is the contentOffset,
we've got our new
contentOffset right there.
And at the time that
this is called,
the bounds has yet
to be changed.
So if we asked this ScrollView
for its current bounds,
we'll find out the
previous contentOffset.
So that let us find out
how much has been scrolled
since the last time
this was called.
So we can do that by just
subtracting the y-coordinate
of those two values.
We're only carrying about
vertical scrolling in this case,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
so that's really easy to do,
we just get a CGFloat
for the delta.
And once we've got that,
we can stretch our springs
by shifting the positions
of each
of those layout attributes
objects.
The layout attributes represent
the current position on screen
of the elements and we want
them to resist scrolling.
So as we scroll up, we
want to shift them back
down by the delta
that we scrolled
and let the dynamic
system pull them into place
and bounce as it goes there.
Then the last bit that we
have to remember to do is
to tell the UIDynamic system
that we've made that change.
Now, this is a bit of
implementation detail--
not implementation detail.
It's an interesting behavior
of dynamics that you have
to understand to really be
able to do this which is
that when you create
attachment behaviors
or really any behaviors
in that dynamic system,
UIKit Dynamics pull off the
values out of your models
into the dynamics
physics world at the time
that you create the
attachment behavior.
But we're going to go and update
that value by changing it,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
we just saw on the
previous line,
by shifting the layout
attributes position.
If we didn't tell the dynamics
system we had done that,
it wouldn't get pulled
into the physics world,
and so the physics system
wouldn't have any pulled spring
to simulate.
So we have to let this
dynamics system know
that we've made this change.
Now, this is actually going
to be done with a method
that Eliza is going to
show you in a minute
that will be available in SID
[phonetic] 2, so you'll be able
to do this real soon now.
To get us started on
building this, Eliza is going
to come back and do our demo.
[ Applause ]
>> OK, so let's add this
springy behavior to the cells
that I've already got in
my color collection view.
In order to do that, we
need to write a subclass
of UICollectionViewFlowLayout,
so I'm going
to add that now, I think.
There we go.
So we will make a new class,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
we'll call it "Springy Flow
Layout" and it's a subclass
of UICollectionViewFlowLayout,
add it to my target.
All right, now in my View
Controller's [inaudible] file
here, I'm going to zoom in a
little bit so that you can see.
I've got this ScrollView which
contains the building view
and the collection
view and then it also--
and the collection view contains
a collection view flow layout.
So to adopt this new subclass
that I'm about to write,
it's going to be simply a
matter of changing the class
of my CollectionViewFlowLayout
to be Springy Flow Layout.
It's pretty easy.
All right, so let's go ahead and
implement Springy Flow Layout.
First thing we're going to
need in this flow layout is a--
let's close this, yup.
We're going to need
a dynamic animator.
And we're going to want to
create the dynamic animator
and fill it with springs.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And we're going to do that by
overwriting the prepareLayout
method of our super class which
is UICollectionViewFlowLayout.
And we're taking advantages of
the fact that in prepareLayout,
our supper class is doing
all the math for us.
It's figuring out where all of
the different cells should go.
And it's doing that here
in the super implementation
of prepareLayout.
So now that that's been
called, we can go ahead
and create our dynamic
animator, if we haven't already.
And then we can ask
the super class for all
of the layout attributes
that it just computed.
And we'll do that by
getting our content size,
that's the collection
view content size.
And then we'll simply ask the
super class to give us an array
of all of the elements-- the
layout attributes for all
of the elements in the Rect
that is our entire content.
Now, this is a convenient
method that I'm using here.
I've got 80 cells and not
that big a collection.
If you had a collection of
hundreds of thousands of things
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
or even maybe thousands of
things, you might not want
to load them all into
memory at a time.
So note that I am cheating
a little bit in this demo.
At that point, you might want
to tile and I'll show you
in a second where
you would do that.
But-- all right, so for now
we've got all of our items
and we're going to
iterate through them
and make springs for each one.
So a spring is
UIAttachmentBehavior.
We initialize it with the item
which is our layout attributes
for that particular cell and we
attach it to the anchor point
which is the item center.
And that center was
calculated for us
by the super implementation
of the flow layout.
It figured out where that
item was supposed to go.
So now that we've got a spring,
we need to set its length.
Now this is the--
this is important.
The spring's length has to
be zero because if you think
about it, as we scroll, the
springs are going to stretch out
and then the content is
going to bounce around.
If the spring's length
was greater than zero,
then it wouldn't be
guaranteed to come
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to rest right at
its anchor point.
It could potentially
come to rest
at an arbitrary location
somewhere near its anchor point.
That would give you
a collection view
with some really weird behavior.
When it came to rest, the cells
would be kind of overlapping
and kind of off center.
So we want our spring
to be length zero.
We also need to set the dumping
and frequency of the spring
which I've done by
trial and error.
These values turned
out to be nice.
And then we need to tell
our dynamic animator
about this spring.
So we add the spring
as a behavior
to the dynamic animator.
All right, so now we
have done this once
when the prepareLayout
method is first called
and that will be good enough.
We need to-- in addition,
we need to implement
these two other methods,
overwrite them, rather.
So there's layoutAttributes
ForElementInRect.
We have to overwrite that method
to tell our collection view
what items are currently visible
in a particular Rect.
And notice that the
super implementation
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
of this wouldn't work
because we're moving the items
around using this
dynamic system.
So we have to ask the dynamic
animator which is keeping track
of where they really are what's
currently available in the Rect
and UIDynamics provides this
nice call through for that
and the same exact logic goes
for this layoutAttributes
ForItemAtIndexPath method.
All right, so finally, we
just need to write the code
to stretch the springs
when scrolling takes place.
And as Josh explained,
we're going to do
that in shouldInvalidate
LayoutForBoundsChange.
Let me just get this to be
higher up on the screen.
OK. So in shouldInvalidate
LayoutForBoundsChange,
what we're going to do is
grab the ScrollView out,
which is just our
collection view,
and find out what its current--
well, how much we just scrolled
because remember that
this, because the bounds
of the ScrollView
change every time
that its contentOffset changes,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
we're going to find this
method getting called
at every frame of scrolling.
So we want to know, what is
the delta that we scrolled
since the last time
this was called?
And then we're going to go
through all of the springs
that we made above and
we're going to stretch them
by moving the item that that
spring owns by the amount
that we just scrolled.
So, we grab the item
back out from the spring
which has an array of items
and then we get the center
of the item, adjust it by the
scroll delta, set it again,
and finally call
our SUD 2 method,
Dynamic Animator Update Item
For Current State coming soon.
And that will cause the
new center to be pulled
into the dynamic
system and it will cause
that bouncy effect
to start happening.
Now, the last thing we need
to do is tell this method
whether indeed it should
invalidate its layout.
We're going to say No because
the dynamic animator is going
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to be moving the items around
and that itself will invalidate
the layout, so we don't need
to double invalidate it.
We can say, "No, it doesn't
have to invalidate 'cause
of the bounce change."
Instead, it will be
invalidated in a second
because of the moving
of the item.
All right, so let's see
what that looks like.
All right, so I'm going to grab
this cell, purple cell here,
and I'm going to
start scrolling.
Now, you'll notice we
have a bouncy effect.
It's not really the
bouncy effect we wanted.
First of all, my
finger, as presented
by this giant mouse pointer,
is coming off the
cell that I grabbed.
See how it gets away ahead
and then when the thing comes
to rest, it's back
under the finger again?
This is really not the
behavior that we wanted.
There's two problems.
First of all, the cell right
under my finger should
track my finger.
Otherwise, you have
this weird effect
where you're not directly
manipulating the content.
And second of all, all of the
cells are bouncing together.
They're not coming
apart as we wanted.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So if you look back at the code,
this is actually not surprising.
We're adjusting the
center of every single item
in our collection view by the
same amount at every frame.
So of course, they
don't come apart.
And that's also why
that content is coming
out from under my finger.
All of the cells are resisting
scrolling by the same amount.
So I'm going to turn it back
over to Josh one more time
to explain the technique
for actually fixing this
and getting the behavior
that we want.
[ Applause ]
>> OK, so we left off on
last time that we're looking
at our video of this stuff
with this exact look here,
but we didn't actually play it.
If we had let go at this point
and allowed the dynamic system
to take over and run
that spring simulation,
we would've seen
exactly what Eliza saw
of everything bouncing together.
Now, of course, if we did
that a little bit more,
you'd see we stretched
the springs
and they all move
the same amount
and everything bounces together.
It's exactly what we saw
happen in Eliza's demo
and clearly not what we
were trying to build.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So what change do we have to
make in order to fix this?
Well, we still want to move all
of the individual attachment
points together the same way
that we thought we were doing.
The thing that we want to do
differently is that we want
to stretch the springs
at different amount.
And we want to stretch them
by an amount that varies based
on how far they are from the
user's finger on the screen.
The ones that are directly
under the finger should stay
under the finger
and track directly.
So we don't want to stretch
those springs really at all
because we don't want that to be
springy, we want it to follow.
And the farther away the
cell gets from the finger,
the more we want to
stretch out those springs.
So that's going to look
something more like this.
The user puts their
finger down on the screen
and starts scrolling,
the cell directly
under it has stayed directly
under it, they've bunched
up in the direction that
we're scrolling towards,
and spread out in the direction
that we've scrolled away from.
And if you look at our green
lines here representing the
spring lengths, you
can tell that the ones
that are farther away from the
finger had been stretched more
than the ones that
are right underneath.
If we let that go, then
they're individually going
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to bounce at different speeds.
Each one's going to have
a slightly different feel
and it'll get that
kind of behavior
that we were looking for.
So if we do that, we
see different amounts
of stretching varying by how
far it is from the finger,
and we get that really nice
bouncing feel we were after.
So Eliza is going to modify our
demo now to get that effect.
[ Applause ]
>> All right, so we have a
ScrollView and that turns
out to be really convenient.
We want to figure out where
did the user put their finger
down so that we can stretch
the springs that are farther
from the finger more
than the springs
that are right under the finger.
So, we'll use the fact
that ScrollViews expose
their pan gesture recognizer
and pan gesture recognizers
expose the location of the touch
in whatever view you want.
So we'll simply say the touch
location is the ScrollView's pan
gesture recognizer's
location in the ScrollView.
So now we know where the
user has the touchdown
and we can take advantage of
that to figure out how far
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that touch location is from
each individual spring.
So, we'll grab the anchor point
of the spring that represents
that particular cell's
resting position.
And the distance from the
touch is just the difference
between the y-coordinate
of the touch location
and the anchor point.
Now we want to scale the amount
that we resist the
scrolling by that distance.
So I'm going to make a variable
here, scroll resistance,
and it's just going
to be a fraction
of the distance from the touch.
Now this is something
you can play around with.
Basically, the more scroll
resistance, the bouncier.
So if I have a lot of scroll
resistance on a particular cell,
then it's going to-- the
spring is going to stretch more
and it's going to bounce more.
And so you can play with
different denominators here.
This one gets us a fair
amount of bounciness.
So now, when we adjust
the center of the item,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
instead of just adding the
scroll delta, what we want
to do is add the
scroll delta times
that scroll resistance
fraction that we calculated.
So when we're at a cell that's
right under the user's finger,
the distance from the touch
is going to be pretty close
to zero, so the scroll
resistance--
rather, the amount that we're
changing the center will be
very small.
And when the touch is
very far from the finger,
we'll be changing
the center by more.
So now there's one sort of--
this isn't quite right as I've
written it because we never want
to change the center
of this item by more
than the scroll delta.
We always want to be basically
adjusting it by something
in between zero and the scroll
delta because if we changed it
by more than the scroll
delta, then instead
of just resisting scrolling,
it would actually be moving
in the opposite direction
from scrolling,
and that would be very strange.
So I'm going to, in
fact, have this at one.
So we want the minimum
of the scroll delta
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and then the adjusted scroll
delta by the scroll resistance.
All right, so with
that in place,
we can go ahead and build this.
Now, if I grab this
cell here and I scroll,
it stays under my
finger, which is good.
And-- oops.
And the content bounces
around exactly as we hoped.
The stuff in the direction of
scrolling gets closer together
and the stuff farther from the
scrolling gets farther apart.
So that was the effect
that we wanted.
Before I turn it
back over to Josh,
because we have another minute
or so, let me just show you
where you would do this
tiling that I mentioned.
You don't want to load all
of these springs into memory
at the same time, most
likely, in real use case.
So here where I'm adding
behaviors to the spring,
what you could basically do is
add only the behaviors that are
in near what's visible on
the screen at a given time.
And if you want to look at--
look back at some of
our previous sessions,
we talked a lot about tiling.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So this is a slightly weird
use of tiling where instead
of tiling views, you're
tiling springs, just this--
you're only making the
springs that you need in order
to control the content
that's pretty close
to what's on screen.
And you can do that by--
over here in the
UIDynamicAnimator header,
you can see that you can
both add and remove behaviors
from the dynamic animator.
So if you wanted to do this
in a slightly more
memory-conscious way,
that would be the place to look.
All right, so back over to Josh.
[ Applause ]
>> So not to point out bugs,
but some of you probably noticed
that when scrolling down,
there wasn't quite
the same bouncy effect
that we were hoping for there.
That is a result of us changing
the demo late last night,
and apparently not verifying
that we had done it correctly.
There's actually-- I have made
that exact same mistake myself
at my desk when looking
at this demo in the past.
There's actually an issue
where if you do the max
in that particular way,
you actually pick the value
that's always greater than zero
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and sometimes we
want to go negative.
So max isn't always
what we want.
Sometimes we actually
want to go min.
We could go back and fix that,
but maybe live debugging it
on stage isn't the
best plan right now.
[Inaudible Remark] You can
go and find our bug later.
>> It would've helped
if I built in run.
>> So over the past few
years, we've had quite a few
of these sessions, as
I mentioned earlier.
Since 2009, there's been
a UIScrollView session
and I wanted to give you
a real quick reference
of the different kinds of topics
we'd covered in that time.
So, I don't expect
that you're going
to read all these
right here right now,
but we've covered
a lot of stuff.
We've got photo browsing,
tiling, infinite scrolling.
We did a really interesting
thing
with OpenGL scrolling last year
so that you could figure out how
to use UIScrollViews within your
OpenGL games and applications.
I'd strongly encourage you,
if you're doing anything
with UIScrollView, to come
back and look through this list
and see if there's anything
that applies to the things
that you're trying to do and
go and, you know, go back
and watch some of these sessions
in the WWDC app that Jake has,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
you know, kindly given
us access to this year.
If you have more
questions about this stuff,
Jake Behrens is the UI
Frameworks Evangelist--
or sorry, the App
Frameworks Evangelist now.
Documentation, of course,
we've got the UIScrollView
Programming Guide
and the Apple Developer
Forums are a great place
to find out about this stuff.
You know, I'm on
there a lot and a lot
of other folks are as well.
There are some related sessions.
If you missed it, there's
the Building User Interfaces
with iOS 7 one that was on
Tuesday, and Getting Started
with Dynamics was
also on Tuesday.
Later today, there's
an Advanced Techniques
with UIKit Dynamics
actually right here at 3:15.
So thanks very much for coming
and please enjoy
the rest of WWDC.
[Applause]
[ Silence ]