WWDC2014 Session 232

Transcript

X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
>> So good afternoon, everybody.
My name is Jeff Watkins.
I'm an iOS Software Engineer.
I'm on the iTunes team and I
have the pleasure of talking
to you a little bit
about Advanced User
Interfaces with Collection View.
Recently, we got some brand
new designs for iTunes Connect.
And these were welcome new
designs for iOS 7 look and feel
and they were really fantastic.
They were brand new,
fresh, clean look and feel
and they gave us a
great opportunity
because we have been
making decisions
for our codes since iOS 2.
And as you can imagine, the
decisions that we made back
in iOS 2 were really
not the same decisions
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
in iOS 2 were really
not the same decisions
that we would be making today.
So, this was an ideal
opportunity for us
to take some time and pay
off some technical debt.
Now, we all know that
that's fancy speak for
"throw away the old code and
write some shiny new code."
But the reality is,
we really wanted
to build a really great
modern architecture
that would take us forward
a few more releases.
You know, everybody
thinks that they're going
to build this shiny,
glittering jewel
of an architecture that's
going to last forever.
But the reality is you get
two, maybe three releases
out of anything you
build and then it's time
to rethink things, but
this was our opportunity.
So, I'm really excited
to announce
that we have some sample code
that goes along with this talk.
But what makes this sample code
even more interesting is this
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
But what makes this sample code
even more interesting is this
sample code is distilled
down from the actual source
code of iTunes Connect.
I have my manager and his chain
of management to thank for this
because this is kind of unusual.
Normally sample code
is something
that you would just sort of
whip together for your talk
and it covers the aspects
that you're talking about.
But it, you know, it
covers the bare minimum.
This actually is full,
rich data sources, full,
rich UICollectionViewLayout
and there's actually way more
in there that we're going
to talk about today.
[ Applause ]
Plus, I'm sure there are bugs.
But more importantly,
we'll be building on it
and improving it
as time goes on.
So, look for all
sorts of additions
to this sample code
as time goes on.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So, we had some requirements
just
like you all probably
get requirements
with your applications and I'd
like to go through those one
by one and take a look at
them and give you a little bit
of a sense of where we were
coming from and, you know,
a little bit sense of
the terror that I felt
when I took a look at them.
So first of all, we had
really complex data.
We started off with iTunes
Connect 2 supporting apps
and books, and that was great.
But now, we were going to
support all of the content types
that the store supports.
So we were going to add music,
we're going to add music,
as well as movies
and TV seasons.
Now, that was going to post a
significant challenge for us,
because each one of
these content types
as you can see has
multiple sections,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
as you can see has
multiple sections,
multiple different tabs
within there, we're talking
about an awful lot
of data that's going
to be a real difficult
time to manage.
On top of that, we really wanted
to have a single
loading indicator.
One of the things that I think
is really tremendously important
is that users know when
the content is available
and ready for interaction.
I really don't like it when an
application has a spinner here,
spinner there, and
I don't really know
if it's ready for me.
So I wanted to make
iTunes Connect really clear
that when the spinner was
gone, we were ready for you.
So, we were going
to have our spinner
where our spinner is going to go
away and there was the content.
Now, most of our designs looked
like UITableView
and this was easy.
We could have done
this in our sleep.
But, it got more interesting
on iPad because we needed
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
But, it got more interesting
on iPad because we needed
to support multiple columns.
Now, we could have hacked
together a solution for iPad
but that didn't feel
right, right?
Here we're trying to create
a future-looking modern
architecture where you
shouldn't consider hacks.
So, we really needed to do
something more sophisticated
for iPad and that's
when I started thinking,
"Let's take a look
at collection view."
Well, if you know
collection view,
you know there are
some limitations
with the standard flow layout.
Specifically, it doesn't
really support global headers
like you have in
table view, right?
And we needed to
support global headers.
We needed to be able to have
this one header that stuck
around as you tabbed
through your content.
So that made me start
thinking, "I'm going to have
to do something here and
I might wind up having
to write my own layout."
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
In addition, we also needed
to support pinned headers.
So, as we're scrolling
through our content,
it is really important that
we able-we'd be able to get
at those segmented controls
so that we could get
to the different
sections of our content.
So we needed to be able
to support pinned headers
and flow layout also doesn't
really support that either.
So, that kind of put
the nail in the coffin
of using the flow layout for us.
And that meant we're
going to have
to do a collection view
layout which I got to admit,
it was pretty exciting.
I also wanted to
add swipe to edit.
This was a new feature in iOS 7
and I thought it would be a
great addition to iTunes Connect
because it gives it that
modern interactive feel
and users can then swipe,
delete Xcode from their list
of favorites and move on.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
of favorites and move on.
[ Applause ]
Also, you know, if you've got a
list of favorites, you're going
to want to be able to
edit those, so we wanted
to support batch editing, and
likewise, you can delete Xcode.
Now, favorites wouldn't
be any good, right,
if you couldn't manipulate them,
if you couldn't reorder your
favorites because it'd be kind
of a drag if you got
a lot of favorites
and they're in the wrong order.
So we wanted to support drag
reordering as well and we needed
to support that with a custom
layout and that was going
to be a bit of a challenge but
I feel like I've been slagging
on Xcode so I'm going to put
it back at the top of the list
of favorites because they really
have hit it out of the park.
Come on.
[ Applause ]
With Swift and all of the
other stuff they've been doing,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
those guys-so, those were our
requirements and that was kind
of a lot, I mean on top of all
the other stuff that the rest
of the guys had to do.
But I want to talk about
the first one first
because without data, right,
iTunes Connect is nothing.
They would just be some
pretty pictures of your apps
and everything else which are
nice but nobody is going to want
to look at, you know,
lines and pretty pictures.
So, getting the data right
is the most important thing
and quite frankly, it was
the thing that I was really,
really convolutedly freaked out
about because five data types
and all these different
sections and, oh my goodness.
So, I really needed to
come up with some way
to minimize the complexity
of all that data.
And the way I took a look
at this is I took all
of the designs that our
HI guys and gals, in fact,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
mostly a gal had come up with,
and we lay them out and I kind
of squinted them, add them
and lay them out side by side
and I realized that there were
some real similarities here,
right?
If you squinted this,
you realized
that we've got some key values
stuff going on here and that
in particular showed up all
throughout the application.
We've also got this Status
section that shows up frequently
and we've got lots of sections
in our layouts that seem
to reappear all over the place.
And I thought to myself "Gosh.
This is really something that
we need to take advantage of."
We really need to be able
to reuse this kind of code.
But the problem is, if you've
been doing collection view
controllers in the past,
you know that there's really
challenges with code reuse
when it comes to data sources.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
The traditional approach
to a collection
of your data source has
you putting everything
in the view controller.
And as a result, you'll wind
up with one of the two things.
First, you can wind up with
a gigantic view controller.
And I'm sure you've all seen
this-not on your projects.
But you wind up with the
view controller that's
like 5,000 lines of code.
And it does everything
including things
that you don't do anymore.
I deleted all that code.
And so, we didn't want
that especially now,
I mean we're starting fresh,
right, we wouldn't want that.
The other option is we could
have had one view controller
for each content
type and that seems
like it's a better
approach but it's really not
because there's really
a lot of things
that the view controller
just does innately
and the way you wind up with
sharing common code is you wind
up pushing it down to
a common base class,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
up pushing it down to
a common base class,
which winds up being
the same thing
as having one gigantic
view controller
because you've pushed it
all to the base class.
So sure you've kind of hidden
it a little bit but now it's
down there and you're
just not looking at it.
And it doesn't at
all solve the problem
of code reuse across screens.
So we wanted a better solution.
Now fortunately, I had
the pleasure of working
on the Game Center team during
the iOS 7 redesign and they came
up with a solution to
this exact problem.
It's called Aggregate
Data Sources.
Now their data was a
little bit less complex
than ours by just a smidge.
But they hit upon the right
answer, which was building
up data sources from
smaller data sources.
There's nothing that says that
a UICollectionViewDataSource has
to be implemented on
your view controller.
In fact, I would encourage you
not to implement the data source
on your view controller.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
In fact, I'd go so
far as to say,
never ever again implement
it on your view controller.
[ Applause ]
I mean, unless you
really want to,
but what we do here
is we implement it
as a general NSObject, and
then we build those together
to build a much more
sophisticated data source
and this goes a long way
to enabling code reuse.
Because as you can imagine,
we've got these little classes
of data sources that
we cobble together
and we reuse them
all over the place.
And as a result, we wound up
with a single view controller
for our product detail screen
that has only 14 methods.
Now, six of those
methods-before you get all upset
that I have 14 methods,
six of those methods,
five of them are building
the five content types
of data sources, one of
them is building the overall
data source.
And then the rest of them are
some action methods that bubble
up our responder chain.
I think there's some, you know,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
I think there's some, you know,
delegate methods in
there and whatnot.
So I mean, we do go
a little overboard.
I probably could
have cut it down but,
you know, we're getting there.
So let's take a look at the four
intrinsic aggregate data source
classes that you'll
see in a sample code.
The first and most
important one where all
of the action is
is AAPLDataSource.
That's the base data source.
That's where we implement the
UICollectionViewDataSource
protocol, as well as a host of
other good stuff that we'll talk
about in a little bit.
On top of that, we've layered
the AAPLSegmentedDataSource
and that can have multiple
children but only one
of them is active at a time.
Think of a UISegmentedControl,
in fact,
there's a good reason
to think of that.
It will vend out a header
with a UISegmentedControl
as part of its base behavior.
Then there's the
AAPLComposedDataSource
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Then there's the
AAPLComposedDataSource
and that will take a
number of children.
They're all active at once
and it manages the mapping
between the external IndexPaths
and the internal
child IndexPaths.
And then there's this
AAPLBasicDataSource.
How many times have you
had just a list of things
and you just want to show them?
Well, that's what the
basic data source is for.
It takes an array of items.
It manages insertions,
deletions,
reorderings, all
that nasty stuff.
It sends out the
right notifications.
It only allows you
to have one section
because there's countless
times where that's all you need
and we wind up using
it all over the place.
So let's take a look in how we
use these four classes to build
up our product details
data source.
So first of all, the product
details data source is a
segmented data source
and it has four segments.
No surprise, one for
Details, one for Episodes,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
No surprise, one for
Details, one for Episodes,
one for Reviews and
one for Trends.
Those correspond exactly
to the UISegmentedControl
in the headers.
That UISegmentedControl
is actually created
by the Segmented Data Source.
Now, the Details child
data source is a composed
data source.
And it has children
for the Status section,
Information and Description.
Each one of those is
its own data source.
The Information was one of
those key value data sources.
The Description is a
special textual data source.
And all of these are
pulling information
out of the product object.
Now, Episodes is just one
of those basic data sources
because we've just got
a list of Episodes.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
because we've just got
a list of Episodes.
But this is where
it gets interesting
because as you can see,
Episodes have a show date.
Now we could take the date and
we could pass it off to the cell
and we could have the cell
create an NSDateFormatter
and we could render and do that,
but we've been told countless
times that's really the wrong
thing to do, right?
So this is what data
source has allow us
to do is we can encapsulate
task-specific logic,
presentation-specific logic.
So, in my Episode's data source,
that's where I have a date
formatter that's specific
to the Episodes and I do that
conversion of the NSDate,
that's in each episode,
into a string before I
jam it into the cell.
And that way, I get the best
performance rather than allowing
that to happen in each cell.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Then Reviews is another
composed data source.
You're probably getting
tired of this but it's great.
Let me tell you.
And the composed data source
for Reviews has one for Ratings
and another one for
the Actual Reviews.
And we'll actually come back
to the Reviews data source
when we look at how things
load in the upcoming slides.
And then Trends is a
custom data source.
It derives directly from
the base data source.
Because we go out and
we fetch the trend data
and then we actually render
it in two separate sections.
One for the Graph and then
another for the Historical Data.
So that's how we build up a
product details data source
from all these little
aggregate data sources.
And that has an additional
benefit.
Because remember I
told that we wanted
to have a single
loading indicator, right?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to have a single
loading indicator, right?
Well, we originally tried to
make our single point of truth
for whether we're loading
via the view controller.
And that's kind of tough because
the view controller really
doesn't know what all
is being loaded, right?
We've got five different
content types, multiple sections
with subsections within them.
Each one loads its own content,
how is it going to know?
Well it turns out, we
have one thing that knows
about everything and that's the
product details data source.
So the answer to
the whole problem
of who knows what's
loading, is the data source.
The data source is responsible
for loading its content.
And when you think about it,
it actually makes total sense.
And if you make the data
sources responsible,
they know just the
data they need to load.
They know exactly how to load it
and they're already responsible
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
They know exactly how to load it
and they're already responsible
for their own task-specific
logic so they can format it,
do whatever they need to and
make it ready for presentation.
So now, the view controller
kicks off the whole process
in -viewWillAppear by sending
a load data to the data source
which in turn propagates
that message
to its children as appropriate.
So for example, a segmented
data source will only send it
to the selected data source,
but a composed data source will
send it all of its children.
And because we're good computer
scientists just like all of you,
we use a state machine to
keep track of everything.
You would, right?
Let's take a look at
that state machine.
[ Applause ]
So the obviously named
AAPLLoadableContentStateMachine,
it's got a few states.
It's not as nasty
as it could be.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It's not as nasty
as it could be.
All data sources start
off in the initial state
until they receive a
load content message.
Once they've received
the load content message,
then they transition into
the loading content state.
That's when we display
the spinner.
That's the only time we
ever display the spinner.
And if you noticed, you
can't ever get back there.
So when they get content
or they get an error
or they receive no content
from their respective sources,
they'll transition into
no content, content loaded
or an error state and will
display the appropriate view.
And we'll take a look at
exactly how that works.
So let's see how this all works
from a data source loading
data off the network
and from a UI standpoint.
So here we are in
the initial state.
And we've got my Cat
List Data Source.
This is from the
sample application
because I am cat crazy.
And yes, I was heartbroken
when we switched
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And yes, I was heartbroken
when we switched
to California landmarks.
Yeah, I thought the Ocelot was
finally going to get its chance.
So were starting off
in the initial state
and we get the load
content message
and we transition
into loading content.
Then the data source, in this
case, it's going to request
out to its server,
"Get me some cats."
The server eventually comes
back and says, "Here you go."
Now depending on what the
response is, then it's going
to make a transition
into the right place.
And let's take a look at what
the UI does in this case.
So here we are in
the loading content,
we're showing the spinner
and let's imagine we
get back some cats.
So here we're going
to display the cats.
Like I said, this is
the sample application.
Assuming everything works
well, this will compile
and run exactly as
planned on your machines.
And we'll see our list of big
cats right there on our devices.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And we'll see our list of big
cats right there on our devices.
However, if you know anything
about cats, they're never
where you expect them to be.
So more than likely, you're
going to try to load the cats
and you're going to be told,
there are no cats here.
Well, the great thing about this
is I had to do no work here,
which is great because cats
do a lot of work for you.
So in this case, all I set
up was the No Content message
and the No Content
title on my data source.
And behind the scenes,
the machinery took care
of everything else.
When I transition into the
No Content state, the layout
and the data sources take care
of presenting the
place holder for me.
It really takes a lot of
burden off of my shoulders.
Now, sometimes, things go wrong.
And then, we go from the
Loading Content state
into the Error state.
And similarly to No Content,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And similarly to No Content,
if we've configured an Error
message and an Error title,
we'll get a placeholder telling
the user what's gone wrong.
So this is a really
great way that we found
to get our consistent UI
but it's a little bit more
than because in most cases,
we're not just loading
one thing, right?
I mentioned, we'd come back
to the Ratings and Reviews.
We load that information
separately.
We fetch the ratings and
we fetch the reviews.
Well, we can't update if we've
got a single loading indicator.
We can't update the
Collection View with the Ratings
and then the Reviews because
that wouldn't look right.
So we've got to update
everything all at once.
And in order to do that,
we needed a solution
that was elegant because
anything less would be
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that was elegant because
anything less would be
just wrong.
So the solution that we came
up with relies on the fact
that we have a parent-child
relationship
to our data sources.
So let's take a look at that.
All of our data sources start
off in the initial state.
They get the load
content message
and then it all transitioned
into the loading content state
at which point, the ratings and
the reviews data sources send
out their request to the server.
Now, we all know that
the servers not going
to respond simultaneously
to both requests.
That's fantasy land.
So what happens is one of the
responses comes back first.
That data source will
process the response
but not update itself.
What it does is it
queues up a block
that actually will do the
update and it sends it
up to the parent chain and
that block will just sort
of hang out there for a bit.
Then, the next data source
will get its response,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Then, the next data source
will get its response,
it does the same thing.
It transitions into
Content Loaded,
queues up an update block.
And then the parent will
discover, "Oh, look.
All of my children are loaded.
It's OK for me to transition
into Content Loaded."
And now I can call
performBatchUpdates
on the collectionView
and schedule all
of those update blocks
safely inside
of performBatchUpdates block.
[ Applause ]
The good thing about all these
is we don't get exceptions
because of our timing
inconsistencies.
And I don't know about you,
but I don't like exceptions.
My boss gets really grouchy.
So to recap, Aggregate Data
Sources were a great way for us
to reduce our view
controller complexity.
Our view controller only
does view controllery things.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Our view controller only
does view controllery things.
It no longer involved
in the data source other
than setting it up.
And it went a long way
to promoting code reuse.
So now our code is scattered
in these aggregate data sources
that we use all over the place
and it isolates task-specific
logic that we use for setting
up ourselves into
the data sources
where they're appropriate.
And we got that single
loading indicator
that we were looking for.
So that was our first
two requirements.
So let's take a look
at the next four,
which necessitated a Custom
UICollectionViewLayout.
Now, I have a confession
to make.
The first time I built the
layout, I was very unhappy.
It worked.
It worked actually really well.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It worked actually really well.
But I am-how shall I put
this, hard to please?
And it didn't work
as well as I wanted.
And the reason is I didn't
collect all the information
that I should have.
I tried to be done and just
move on to other things.
So the message I want
you to take away from all
of this-and I'm sort
of skipping ahead
to the summary before I even
start-is do your bookkeeping.
Get all the information you can.
And at the very end, run
instruments to make sure
that you have enough memory
and you're not using
too much resources
and then prune back the
information you're keeping
but keep it all upfront.
So let's take a look at
the information I kept,
the information I didn't keep
and what I should have kept.
So first, what did I need to
keep and where did it belong?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So first, what did I need to
keep and where did it belong?
So obviously, we got these
great designs from HI
and they're just
pictures, right?
We need to interpret that and
figure out what did we need
from that to actually layout
cells and headers and footers
and supplementary views
and decoration views
and the whole nine yards.
And then, kind of as a
footnote, where does it all go?
And I want to address that
first, get it out of the way.
Data sources vend
visual information.
They vend views.
Design metrics are
visual information.
I put them in the data source.
Partly because the data
sources are hierarchal
and they've got default metrics
and they've got section-based
metrics,
it just made sense
to put it there.
I could have put it in
a parallel structure
but then I would have
parallel structures
and that would have
been too crazy.
So, they're in the data sources.
That's where you'll find them.
So let's take a look
at the section metrics.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So let's take a look
at the section metrics.
So here, we've got some Big
Cats and naturally we're going
to want to know the rowHeight.
We also want to know the
backgroundColor, right?
A lot of our sections
can be gray.
Some of them are white.
None of them are garish colors.
Obviously, we have some
that have separators
and separatorInsets but not all
of our sections have separators.
So we needed to be able to set
that on a per section bases.
We also needed to be able to
set a selectedBackgroundColor.
The way we determine whether or
not a cell appears selectable is
by the selectedBackgroundColor.
Whether it actually is
selectable is in code.
Now remember, we also support
multiple columns so we needed
to know, based on the section,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
how many columns it
should support and whether
or not it should show
a column separator.
For the headers and
footers, we similarly needed
to know the height, although
most of the time we set this
to zero which means
figure it out yourself,
thanks to auto layout.
We also wanted to know the
backgroundColor but most
of the time we set this to nil
which means inherited
from the section.
And we also want to specify
a padding because one
of the things that we saw
in our designs a lot was
exactly the same header
but with a little bit more
space between this instance
of the header and that
instance of the header.
And yeah, we could
define a subclass
to give us a little extra space
or we could just have
another metric that says, "Oh,
here we're going to have
10 points of spacing,
there we're going to have 20."
So we added padding.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It actually wound up being
hugely helpful when we needed
to align things horizontally
as well.
So this gave us all the
information we needed
to lay things out in
our actual layout.
But some of this information
needed to be passed along
to the cells and the
headers and, to do that,
we had some Custom Attributes.
And I'm sure you all know
that you can create subclasses
of the layout attributes so
that's exactly what we did,
and we wound up with four plus
a few more Custom Attributes--
first of all, backgroundColor
and the selectedBackgroundColor,
and padding but also
pinnedHeader.
At one point, we thought
that we wanted our headers
to respond to being pinned.
For example, if we'd had a
navigation bar that was blue,
it would make perfect sense for
the header to reach the top pin
and change to be blue,
and it would need to know
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and change to be blue,
and it would need to know
that it was pinned
in order to do that.
So those were the custom
attributes we decided to define
for headers and footers
and our cells.
Now remember, we also needed
to support Global Headers.
And to do that, I'm
going to let you
in on what I thought
was a secret.
It turns out it's not.
We're all familiar
with NSIndexPath normally
having two indices,
one for Section, one for Item.
Well, it seems that you can
also create an NSIndexPath
with one index and
when you do that,
you're creating a Global
Supplementary View indexPath
or a Global Decoration
View indexPath.
It makes no sense two create
these four items so don't try.
But this is how we
separated our global headers
from everything else.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
from everything else.
When we initially tried
this, I tried to lump them
in with section zero and
it just made no sense.
So now, I'm able to distinguish
them from everything else
and we treat global
headers differently.
When you look at the
sample code, you will notice
that the global headers
actually never go off screen.
They pin just underneath
the navigation bar.
They're treated as what we'll
call special attributes.
And they get updated as you
scroll so that they stay
in place and they do all
sorts of funny, fancy things.
Now, the code needs to
be a little bit smart
so that you check the
length of your index path,
so that you don't accidentally
call section and item
for these global index paths.
But other than that,
you're good.
Now, building a layout.
Earlier today, you might
have heard Olivier mention
that you want to be really
careful about using invalidation
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that you want to be really
careful about using invalidation
or you want to be really careful
to use invalidation,
I should say.
And that's exactly the case.
You might be tempted to
build the most perfect,
single-pass layout engine.
Don't, because what you'll wind
up with is absolute
efficiency the first time.
And then someone will come in
and-the collection will come in
and say, "Hey, this
cell has changed."
And you'll have to
re-layout everything.
Or the origin has changed
because you've scrolled
and you'll have to lay out
everything and you'll just drop.
Your performance will die.
So let's take a look at the
pseudo-code so to speak,
for the layout that
we came up with.
And then, we're going
to show you a little bit
of how we snapshot the metrics
for one of our sections.
And I'll think you'll get a
good sense of why breaking this
up into sections is
really important.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
up into sections is
really important.
So first of all, if the data
source is changed significantly,
like it's a totally different
one or the total number
of sections have changed, we
just throw everything away.
And the reason for this is it's
probably more efficient for us
to just re-compute
everything than it would be
to compute the deltas because
our data sources are actually
pretty small.
If you've seen iTunes Connect,
you know that the actual
content isn't that big.
So, yeah, I could probably
come up with a clever way
to compute deltas
and everything else
but this wound up
being just easier.
And in the long run, getting
it done is sometimes just
as important.
Next, if the collection view's
width has changed or obviously
if I threw everything away,
then I need to regenerate all
the layout attributes as well
as collect all of the
special layout attributes.
So all of those global
headers and any pinned headers
that might have been
defined within the content.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that might have been
defined within the content.
So, I make that single pass
and I layout everything.
And then, if the origin has
changed because we've scrolled,
I update the position of any
special layout attributes.
Now when I go back and I
update all of this code
to support iOS 8,
I'm going to fix this
because I'll use the
new invalidation context
and this all will be different
but this is how we do it today.
So let's take a look at how
we snapshot the metrics.
So we have this hierarchal
structure of data sources.
And on your right, is the
product detail screen.
So we start with Section
0, which is the Status,
and we begin our snapshot at the
very least specific data source,
which is the Segmented
Data Source.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Now in this case, these are
actually the default metrics
for the entire product details.
And that's rowHeight equals 44.
BackgroundColor equals
light-grey.
That means everything
is rowHeight equals 44,
backgroundColor equals
light-grey.
So that's what we start with.
Next, we move to the next
most specific data source
which is the composed
data source for details.
It just so happens that
it doesn't define any
metrics whatsoever.
Not default metrics, not
section-based metrics.
So then we can move on
to the status data source
which has section-based metrics
specifically for that section
of rowHeight equals 60
and selectedBackgroundColor
of mediumGrey.
Because remember, we
do the selection based
on the actual color.
So, we have these
overrides for rowHeight
and we have a new attribute for
the selectedBackgroundColor.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and we have a new attribute for
the selectedBackgroundColor.
So you can imagine, if we
had to go through this,
every time we move the origin
of the collection view,
we'd get terrible frame rates.
So, it's really important
that we only do this
when it's absolutely necessary.
So we only do it when the
collection view changes.
But then, we go through and
we update our attributes
when we actually scroll.
So I spoke earlier about
Optional Layout Methods
and this is where my
whole confession comes in.
I did these out of order.
I know I spoke to some
of you in the lab earlier
and I gave you some
specific details.
I started with
-initialLayoutAttributes
and -finalLayoutAttributes and I
realize that, "Oh, my goodness.
In order to do
-initialLayoutAttributes
and -finalLayoutAttributes, I
need the update information."
So let me implement
-prepareForCollection
ViewUpdates and I'll just
grab the array of updates.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
ViewUpdates and I'll just
grab the array of updates.
I'll just stash those over
here because I need them.
And each time I would get called
for -initalLayoutAttributes,
I'd walk the array and I try
to figure out what was going on
and quickly my head exploded
because it was just too hard.
And I admit, I deleted
all the code.
I went on to other things
and it was weeks before I was
willing to come back to it.
And fortunately, in those weeks,
I talked to a lot of people.
I read a lot of the
documentation
and I finally figured out
what I was doing wrong.
And what I was doing wrong is
I wasn't doing my bookkeeping.
I wasn't collecting the
information that I needed.
And so let's talk
about that information
and more importantly let's talk
about the information
in the right order.
So this it turns out
is the right order.
Prepare for layout.
You probably already implement
this if you're doing a layout
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
You probably already implement
this if you're doing a layout
but there's some information
that we need to capture here,
not just building our layout.
And then -prepareForCollection
ViewUpdates followed
by -targetContentOffsetFor
ProposedContentOffset,
which is quite a mouthful, and
then -initialLayoutAttributes
and -finalLayoutAttributes.
So I'm going to talk
about -prepareLayout
and -prepareForCollection
ViewUpdates together
because they do sort of pair up.
First, -prepareLayout.
So here's my layout,
very colorful.
And when I get -prepareLayout,
what I do is I take a snapshot
of the current layout and
I keep it as my old layout.
In the process of building my
layout, I have a lookup table
between all the IndexPaths
to items, all the IndexPaths
to supplementary views, all the
IndexPaths to decoration views.
Remember I said,
keep everything.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Remember I said,
keep everything.
Well, when I get called
for -prepareLayout,
I duplicate everything.
I stash it as the old layout.
Then, in -prepareForCollection
ViewUpdates,
I create a lookup table
for deletions, insertions
and everything that
was reloaded.
And I run through
the array of updates
and all the deletions
get tracked
and all the insertions
also get tracked.
So now, I know everything
that's going on in my layout.
And then before I'm done
with -prepareForCollection
ViewUpdates,
I calculate the delta in the
height of the two layouts
as well as the change in
where the offset of the start
of my content should be.
Because I've got
content that's pinned
and possibly scrolled
off the screen.
It gets a little bit tricky.
And for the sake
of the examples,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And for the sake
of the examples,
we're going to ignore all that.
But that all gets done
right here before I return
from prepareForCollection
ViewUpdates.
So, let's take a look at one
of the other methods
I didn't implement
and later wished I had, the
-targetContentOffset method.
Now, the documentation says that
this adjusts the scroll offset
and I didn't understand
viscerally,
how important this was
until I saw it in action.
And so I have animations
that I'm going
to show you just how
important this is.
It's used by my layout actually
to calculate the pinning offset
and the delta that I use
to prevent unwanted motion.
And the trick is I get
this calculation correct,
which took a little while,
but then I used it
in so many places.
So let's take a look at
that exactly how that works.
Before I implemented this,
here I've got my old content
and it's 320 by 1000 and
I'm scrolled all the way
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and it's 320 by 1000 and
I'm scrolled all the way
to the bottom.
And I put in some new content
which is considerably shorter
and collection view, remember,
does a lot of work for us
and it says, "Hey,
that's not valid.
I'll help you out.
I'll adjust your
contentOffset to 0, 165.
I'll animate everything
down for you."
Well remember, I'm
not easy to please
and I don't want it there.
I want to be able to see
the new content beginning
at the new content.
That's kind of why
I put it there.
So, that means to me that I
need to change my contentOffset.
So, after implementing
target contentOffset
for proposed contentOffset, here
I've got my old content again
with the original
contentOffset of 0, 432.
And this time, I propose-give
a new contentOffset of 0,
0 and notice, whoosh,
everything slides down
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
0 and notice, whoosh,
everything slides down
and I can see my content
at the correct position.
But I'm still not happy because
my content shouldn't be moving.
If you've seen the sample app
or you've seen iTunes Connect
or if you've seen Game
Center, you actually know
that I wanted my content
not just to come in.
I wanted my content to
slide in from the side
because frankly that's cool.
And if I've got content
that slides in from the top
and slides in from the side, my
users are going to get seasick.
I don't want that.
I'm going to get
one-star reviews for that.
So the solution is -initial
and -finalLayoutAttributes.
That function that I tried
to do first is the final one
that I needed to implement.
And just a reminder,
initialLayoutAttributes is
called if the view will be
on the screen after the update.
finalLayoutAttributes is
called if the view was
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
finalLayoutAttributes is
called if the view was
on the screen before the
update and they're both going
to be called for a view
that remains on the screen.
And I know you're not
going to believe this
until you see the code, but it's
actually really simple once all
the information is
been processed.
So, let's look at some more
animations before I let you see
the code.
On the left here, we see what's
happening to the viewport.
On the right is what the
user actually sees, OK?
Without initialLayoutAttributes,
the blue content is stuck
where it really is and the
viewport slides up to meet it
and that makes that
unwanted animation.
However, after I've implemented
initialLayoutAttributes,
you'll notice that the
new content is synced
up with the viewport and
as the viewport transitions
to its new location, the
new content animates with it
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to its new location, the
new content animates with it
and appears to stand still.
Now, everything is
moving but nothing appears
like it's moving, and
that's what's important.
So, let's take a look
at the actual code.
I promise you it's
actually really simple.
So, this is the first half.
We start off by getting the
section then we get a copy
of the layout attributes,
and obviously we need to copy
because we're going to modify
them possibly and we don't want
to modify the real attributes
because then we'll be
modifying the real attributes
and then our cells
will be horrible.
So then, we just
determine whether
or not this particular item
was inserted or reloaded.
Remember I said we did
all this calculation
in -prepareForCollection
ViewUpdates,
and now that means we've only
got these two lines of code.
Then it was inserted.
We changed the alpha to be
0 so it's going to fade in.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We changed the alpha to be
0 so it's going to fade in.
If it was reloaded and it
didn't exist in the old layout,
remember I created the
snapshot of the old layout.
So, if there is no item at that
IndexPath in the old layout
when it was reloaded,
we want it to fade in.
It's perfectly OK.
If there was an item there the
previous time then it's just
going to change.
I'm OK with that.
And then finally, we want to
offset the origin of the item
by the delta of the
content offset.
And that way, everything is
going to stay exactly in place.
So, to recap, bookkeeping
is critical
to making your layout work.
If you don't have the
information you need,
there's no way you can
make these methods work.
And then the optional methods
really make a huge difference.
They're the difference between
a layout that technically works
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
They're the difference between
a layout that technically works
and a layout that's just great.
So, middle four requirements.
I think we did a pretty
good job with that.
The last three I thought were
just going to be, you know,
a little something extra that
we'd add to the application.
And they changed from being
a little something extra
when they started to
take a little bit of time
so let's take a look at them.
In order to add Swipe to
Edit, obviously we needed
to add actions to our cells.
And there's any numbers of ways
we could have added actions.
But in this case, we add
them directly to the cells.
And here we are because we're
going to use this as an example.
We're adding two actions
to a cell, makeFavorite
and swipeToDeleteCell.
You can see that each action
has a title and a selector.
Those selectors when they
get-- when the actions invoked,
bubble up the responder chain
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
bubble up the responder chain
and our view controller
catches them.
Now, for swipe to
edit and batch editing
and the whole nine yards,
we needed some additional
custom attributes.
We needed the columnIndex,
the editing state, and whether
or not a cell was movable.
And I think those will become
apparent why we needed them
in just a moment.
So, for Swipe to Edit with
one column, this works exactly
as you would expect, right?
Swipe over, you get the two
buttons, you tap on a button,
actually goes up
the responder chain,
view controller catches it.
Hooray, all is good.
Now, in the case of the delete
action, that's all handled
by the base view controller.
For two columns, it gets a
little bit more complicated
because, just like Game
Center, we wanted it to appear
that our cell was sliding
underneath the other content.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that our cell was sliding
underneath the other content.
And in order to do that, we
needed to have the column index
as an attribute on the cell.
So when the column index is not
0, we display a little gradient
and therefore everything looks
like it's sliding underneath.
So for batch editing,
we have an attribute
for whether it's editing.
And when that changes to No, we
animate out our editing controls
and if movable is Yes, then
we'll also display the gripper.
And movable is actually
determined based on a query
to your data source or
our data source really.
And as you would expect,
you tap on the twisty thing
and out come the
editing controls.
And because I am much
addicted to state machines,
it is all controlled by
another state machine.
This manages all of our
gesture recognizers,
the UIPanGestureRecognizer, the
UILongPressGestureRecognizer.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
the UIPanGestureRecognizer, the
UILongPressGestureRecognizer.
The states are somewhat complex.
The diagram gives
you an indication.
The reason for that
is that we have
to respond to external stimulus.
So for example, if you have
a toolbar that has an Edit
and a Done button that toggles.
If you're editing and the user
taps Done, at any given moment,
we have to shut it all down
and transition back
into the idle state.
And so, that means that things
are a little bit more complex.
I'd love to revisit this and
see if I can't, you know,
simplify it, but it
works and that's,
you know, a key criteria.
It only works with our
layout in our cells.
I tried at some length to
generalize it a little bit more,
but it works and
that's a great feature.
So, Drag to Reorder.
It required some layout changes.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It required some layout changes.
In order to make
everything work correctly,
we need to add a layout gap.
So as you're dragging
the cell around,
we needed to split apart
the existing cells.
And we also needed to take the
cell that you were dragging
and mark it as Hidden.
And we also needed to make sure
that our layout calculation
was fast enough.
I'm not going to pretend that
it's as fast as it's going
to get because I'm not done
yet, but it was fast enough.
It feels reasonably fluid.
It's good enough for
the first release.
Now, it does require
data source support.
So by default, the data source
normally answers, "Nope,
you can't move that one.
Nope, you can't edit
that," so on and so forth.
So, in order to actually
implement drag reordering
in your code, you're going
to want to say, "Oh yes,
you can move those thing.
You can drag those things."
And so there are some methods
on your data source that need
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And so there are some methods
on your data source that need
to have the right responses.
But it really was an
incremental change to our layout
and an incremental change
to our data sources.
So, that's the last
three requirements.
And they were a little bit of
a challenge but they were just,
you know, a little bit extra.
So in summary, the aggregate
data sources went a long way
to simplifying our
complex designs.
When I first saw the
designs that HI gave us,
I was a little bit terrified.
There was a lot there.
We had a very short
time and I'm glad
that we found a way
to make it easier.
And for UICollectionView?
Bookkeeping, bookkeeping,
bookkeeping.
Keep all the data.
Don't let it out of your sight.
At the very end,
when you're concerned
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
At the very end,
when you're concerned
that you're using too much
memory, fire up Instruments.
Let Instruments tell you if
you're using too much memory.
And then finally, Swipe to Edit,
Drag Reordering-they're
just incremental things.
You can do it.
And they're sample code.
Pull it out of there and
make it part of your own.
So, for more information,
talk to the incomparable
Jake Behrens.
I'm told he has great shoes.
Documentation, take a look
at the iOS documentation
online and the Dev Forums.
There's great material there.
We've been known to
hang out there as well.
There were related
sessions this morning.
I'm sure they'll be
online this evening.
So, thank you so much for coming
and I hope you enjoyed
this evening.
[ Applause ]