Transcript
>> Josh Shaffer: Alright, good morning guys.
My name's Josh and I'll be joined
in just a little bit by Eliza,
who once again has some more incredible
demos to share with you guys this year.
If you came to see the mastering iPhone-- the
Mastering iPhone Scroll Views last year at WWDC,
then you know we started out that by talk by getting into
a lot of the basics of how to configure a scroll view
for just a bit of scrolling and zooming, the really
simple parts that, you know, that everyone has to do
when they start out to configure a scroll view.
So this year we're actually going to skip
over all of that introductory basic stuff.
So if you weren't here last year or you're kind
of new to this, don't worry, it's no problem.
There's plenty of sample code available,
and the documentation is excellent
and it's actually really easy anyway.
There're only a couple of things to do to configure it.
But what we really want to do this year is jump right in to
some of the more advanced, exciting things that you can do
with UIScrollView, when you really start
to use them in your applications the way
that Apple uses the UIScrollView in our applications.
So we're going to frame the rest of the talk around building
a photo browser that behaves exactly like the photo browser
that you'll find in the Photos application in iPhone OS.
Now, I see some of you kind of looking at me
thinking, "I don't have any photo browsers in my app.
So maybe I just kind of get out here."
But don't worry about it, it's not a problem.
All the techniques we're going to talk about are perfectly
applicable to plenty of other types of applications.
In fact, the things we're going to discuss
are used in Safari, in Maps, in Stocks,
in Weather and all of these different
applications on iPhone.
So we'll take a look at how we can use them, and we'll build
a photo browser and then you guys can go back and figure
out how you can use them in your own applications.
So let's get started by talking about
how we'll configure our UIScrollViews
in order to behave like the photos application.
So what is it that we want to get?
Well hopefully, you've all already seen this.
The basic idea is going to be, that you can
scroll around on photos and zoom in on them
and view just these large great full screen photos.
So of course, we've got the full screen
photo is the most important part of that.
And you know that your users expect to be able to
swipe left and right to go between multiple photos.
So let's add one to the left and one to the right there.
Then they expect to be able to just swipe and have it move
over and swipe back to navigate around between their photos.
When they're at a photo, they want to be
able to zoom in on that photo by pinching
or by double tapping or two finger tapping.
Now, you'll notice something interesting happened here.
When I zoomed in on that photo, the two photos to the
left and to the right didn't actually move to make room
for the thing that I just zoomed in,
they're off screen and not visible anyway.
So it doesn't really matter where they are.
Now this isn't just for the slide.
This is kind of a setup to give you an idea of what to
expect for how we're going to end up implementing this.
A lot of people start out by thinking that they really
need to move this entire set of photos as one large plane
and it's not entirely obvious how
you might do that with UIScrollView.
So a lot of people end up going back to UIView and
starting from scratch and just subclassing UIView
and implementing raw touch handling
and doing everything from the ground
up implementing an entire scrolling subclass.
Even if you didn't do that, a lot of people we find
end up going out and looking for third party frameworks
that provide this kind of functionality,
because it seems really hard
and it's not something you'd want to take on, on your own.
Well the important thing that we really want to
get across while we're talking about this today,
is that you really don't have to do this.
You don't have to start from scratch you
don't have to go to third party frameworks.
You can use UIScrollView to get all this
behaviors that we're going to talk about here.
Alright, so your users zoomed in on the photo, once they're
viewing it large, they can then swipe around on that photo
to scroll around and view different parts of it, right?
And when they get to the edge, they'll continue to
pull the photo and it will swipe back to next one
and shrink the one they zoomed in
back down to its original size.
OK, so that's the type of behavior
we're trying to end up with.
What kind of view hierarchy to we
have to build in order to get to that?
Well, we're actually going to take the two parts that
we're trying to accomplish and separate them out.
We're going to look at paging independent from
zooming and consider them to be two different things.
So we'll start out with implementing the paging.
And to do that we'll use a paging UIScrollView
and if you use UIScrollView you know
that that's just a normal UIScrollView
where you set the paging enabled bit to yes.
And we'll create one that covers the entire iPhone screen.
So in this case it's going to be 320 by
480 points, so it fills the entire display.
Now we'll configure this for the sample we're
looking at here to display three photos.
So if you've used paging scroll views before,
you know that the page width is determined
simply by the bounds width of the scroll view.
So in our case since our bounds width is 320 points,
we're going to have page width of 320 points.
So we'll multiply by that 3 and set our content size
to 960 by 480, so we can swipe between three pages.
Now that's all we actually have to do, to get
the paging behavior that we're trying to add.
So on to zooming.
For zooming, we're going to add separate UIScrollViews that
handle just that zooming and panning on the zoomed images.
So we're going to add a new UIScrollView subview
of our outer paging Scroll View
that again covers the entire screen.
So it's going to be 320 by 480, and it will be a subview
of that paging ScrollView filling the entire screen.
And since we've got two other photos that we're trying
to display as well, we'll add two more to the left--
to the right rather, so that you can see a zooming--
allowing the user to zoom in on
each one of these different photos.
And finally, we need to actually display the photos.
And if you've used UIScrollView to add zooming support
before, you'll be familiar with the delegate method view
for zooming in Scroll View which is how you implement
zooming in a Scroll View, you basically return some subview
of your UIScrollView that you want the user to be able zoom.
So we're going to need to add a subview, so we'll just
add one UIImageView to each of these zooming scroll views,
and we'll return those from the view for
zooming and scroll view delegate method
so that they're the things that get zoomed.
So we've kind of a built a bit of
a layer-- a set of layers up here.
It's gotten a little bit complicated
and you can't see it all anymore.
So let's step back and take a look
at everything we just did.
We've got our outer paging scroll view at the bottom.
That handles just the paging.
We've got subviews to handle the zooming
and those are also UIScrollViews.
And then finally, we've got the UIImageViews subviews
of the zooming scroll views that
are actually displaying the photos.
The combination of these three things just like
this is all you actually have to do in order
to get the exact same paging, zooming, scrolling
behavior that you see in the photos app in iPhone OS.
Just adding them as subviews takes care of everything.
So the one thing that's missing before we go and
take a look at how you'll actually implement it is
that the photos application, you'll notice actually
separates the photos a bit, there's a bit of black padding
between each one of the photos to make
it very clear that there's a border
where one photo ends and where the next begins.
In our pictures of frogs here, they're all pretty green
and as you're scrolling between it's not entirely obvious
where one ends and the next begins,
because they kind of bleed together.
So in order to make them stand apart a bit,
we actually have to increase the page width
so that there's more space between each page.
But as I just said a minute ago, the page width
of a scroll a scroll view is determined
by that scroll view's bounds width.
So in order to get the page width bigger, we actually have
to increase the size of that outer paging scroll view.
So I'm just going to change the bounds from 320
points up to 340 points and keep it centered on screen
so there'll be 10 points hanging off on the left and
10 on the right but they're off the side of the screen
and there's not going to be displayed there anyway.
So it's really just to increase the page width.
The zooming scroll views will stay the same size.
So once we do that, you'll see they'll kind
of spread apart and we'll get a bit of padding
between each photo, to frame the photo against black.
Now it's a little hard to see so
we collapse it back down again.
You can start to see that we now we can view a bit
of that outer paging scroll view from
behind of the zooming scroll view.
Zooming scroll view is still 320 points,
paging scroll view is now 340 points.
And each of those zooming scroll views are just centered
within their respective 340 points
of the paging scroll view.
So that's the configuration.
There's really not much else to it.
With that, let's have Eliza come up and
show us how to actually build this in code.
>>Eliza Block: Hi.
I'm Eliza and I'm an Engineer on the Spring Board team.
I'm going to show you how you can set this up
in code to do exactly what Josh just described.
So we're going to start with a simple view base
application and we're going to do almost all of the work
to configure the zooming and paging in the view
controller subclass that's our root view controller.
So I'm going to switch here to the demo machine.
Alright, so this is the header file for our view controller
and I'm going to add one instance variable to start us off,
which is the paging scroll view
that we're going to be using.
I'm now going to switch to the implementation,
and we can actually do all of the setup
in the Load view method of our view controller.
So we'll start by creating the Paging
Scroll View that we just declared.
We need to figure out what its frame is going to be.
So let's start with the screen bounds.
But as Josh explained, we're going to want to have that
paging scroll view hang off the sides of the screen
by 10 pixels so that we'll leave some space on either side.
So I'm going to subtract 10 from its X
origin and I'm going to add 20 to its width.
And the effect of that is it will now hang off
10 pixels on the left and 10 pixels on the right.
So let's create the paging scroll view with that frame.
We're going to set a few properties now.
We need to set paging enabled as Josh explained.
We need to set the background color to black.
And finally, we need to set the content
size of this paging scroll view now.
The content size is the property
that determines the scrollable area.
So we want to make it wide enough to accommodate all of
the pages that we're going to insert in a minute here.
So the width of this content size is going to be the size of
the width of a page times the number of images that we have
and I've got a convenient method here, image count,
which just returns the number of
images that we're going to display.
And the height will just be the size-- the
height of the frame because we're not going
to allow for scrolling in the vertical direction.
Alright, we're in the load view method.
We need to produce a view.
So we can actually use this paging scroll view as our view.
So I'll just set our view to the paging scroll view.
So now the paging part is totally finished.
This is going to work just fine but
it doesn't have any content yet.
So the next thing we need to do is add some pages.
Alright, now as a first pass, let's just go through
all the images we have and for each image we're going
to make a page and insert it into the scroll view.
So I'm going to just iterate through my images.
And for each image, I'm going to
create a zooming scroll view.
And now, I've made a custom subclass of UIScrollView
called imageScrollView and I'm going to use it.
What this does is it sets up the zooming for you and--
but-- so I'm not going to show you the details of that now.
But if you want to take look at it
in the sample code you can go ahead.
Basically, it creates a zooming scroll view exactly as you
would if you weren't embedding it into a paging scroll view.
It's just a straightforward zooming scroll view.
And it sets itself up with the right minimum
and maximum zoom scale and everything.
Alright, we're going to configure this page
for the particular index that we're at.
And that's just going to set the
frame of the page appropriately.
So the first page is going to go at the beginning of the
content of the paging scroll view and then as we go forward
and index this we'll position them in a row.
It's also going to find the image for that index and
tell the zooming scroll view to display that image.
Finally, we need to add that page as a subview of
our paging scroll view and that's pretty much it.
We can just go ahead and build this.
[ Pause ]
OK. So, we have a page here.
We can zoom in and out on it.
So the zooming part is working.
As we get to the edge, we can page over and
you can see that the paging works as planned.
There's one drawback here which is that the-- our
image when it gets to the landscape dimension is
up at the top which isn't really what you'd want.
So you'd actually kind of want as the image gets zoomed
out to be smaller than the screen you kind of want it
to be centered in the screen rather
than hugging the upper left corner.
So that some of you might be aware the default behavior
of UIScrollView is that as the image gets smaller
than the bounds of the Scroll View it hugs the upper left.
Actually a lot of people asked us last year after
our session whether there was a good way to fix that
and what I'm going to do right
now is show you how to do that.
So we're going to modify this so that the image
stays centered as you scroll out, zoom out on it.
Alright so to do that, we're going to switch
over to this imageScrollView subclass.
So I'm going to go grab the implementation file for that.
And we can do that by overriding the layout subviews method.
Now, the advantage of the layout subviews method is that
it's called at every frame of both zooming and scrolling.
So if we want to keep a view centered,
this is the perfect place to do it.
So the first thing to do when you're
overriding layout subviews
in a UIScrollView subclass is to remember to call super.
UIScrollView does a lot of important configuration in
its layout subview methods so don't forget to do that.
And then we're going to need to figure out what is the size
of the bounds that I want to keep this thing centered in
and that's going to be the bounds, my own bound
size, since I'm in this case, the UIScrollView.
And then we need to grab the frame that we're going
to want to center which is the image view frame.
So now we're just going to go ahead and center
this frame both horizontally and vertically.
So here's the horizontal direction.
Alright, as you're zooming out so
that your image is getting smaller,
you only want to start centering it once it has
started to be smaller than the width of your bounds,
otherwise you want to kind of leave it alone.
So what we want to do is check.
Is the frame smaller in width than the bounds?
If so, we're going to adjust the origin of
our frame to keep it centered in the bounds.
If it's not smaller, we're just going to put it back at zero where it started
so that we don't leave it centered as we zoom back in again.
Do the exact same thing for the vertical.
And finally, we just need to use
the new frame that we calculated.
So I'm going to build this again.
[ Pause ]
And now, as I scroll over, you can see that the
landscape images are centered as we'd hoped.
And in fact if I zoom out on one of these, you can see
that it hugs the center rather than the upper left.
Alright, so before I turn it back to Josh,
I want to just signal one big problem
with the application as we've run it so far.
So I'm just going to open the activity monitor
and take a look at our memory consumption here.
So, alright, let me see whether I can zoom in
on this to show you the real memory here is--
we're using 400, almost 450 megabytes, the real memory.
Now the reason for that is that
these images are pretty large.
They're 6 or 7 megabytes compressed which translates
to somewhere between 20 and 40 megabytes uncompressed
and what we did was we loaded every
single one of them upfront.
We added every single one of them to our paging scroll
view so we have them all open in memory at once.
And an iPhone doesn't have this much memory at all.
So you would crash before you even started if
you were to do it this way on the actual device.
So I'm going to turn it back to Josh.
He's going to talk a little bit
about how we can avoid this problem.
[ Applause ]
>> Josh Shaffer: Alright, thanks Eliza.
So, now we've got all of our behaviors
exactly as we want them except for the part
where our users can't really see them because the
app crashes before they can actually launch it.
So, let's try and fix that problem so
that somebody could actually use our app.
Now we talked a bit about one of these approaches
last year in the Mastering iPhone Scroll View session.
And what we talked about was tiling
your content using subview tiling.
So we're going to talk about two
different approaches to tiling this year.
The first will be subview tiling again although we're
going to talk about it in a different context and use it
for a different purpose than what we used it for last year.
And then we're going to talk about
CATiledLayer and drawn tiling.
So why do you want to tile first of all?
Well, the first reason is what we just talked about.
You may want to display more content
than you can actually fit in the memory.
But you may also want to download additional
pieces of content as you need them.
The Maps Application on iPhone OS for example downloads just
individual tiles of whole world map at multiple zoom scales
and different resolutions, more data than
would probably even fit on the phone.
So you may want to tile if you
have to do something like that.
But it also improves load time.
I'm not sure if you noticed when
Eliza was building and running there.
But it actually took quite a while
for that app to launch the first time
because it was uncompressing all those
images and that was on a really fast Mac Pro.
On your device, it would take so long that you would--
your app would get killed before it even launched
anyway, even if it didn't run out of memory.
So we really don't want to do that.
Alright, so two approaches.
First, we've got subview tiling.
So we'll leave our little frog for
later and we'll come back to him.
Now if you've used UITableView before, you've
already seen subview tiling in one way.
Table view, you know, when you implement your cell for
rowAtIndexPath method, the first thing that you try
and do is dequeue a reusable cell with an identifier.
And what's that doing is basically implementing
subview tiling for you on your behalf.
So, as your user scrolls through their table
view, cells move off the top, you dequeue them
and put new content in and they scroll in on the bottom.
And this happens repeatedly.
So we'd really like to do basically
the exact same thing for our photos app
as the user is paging horizontally
one photo moves off screen.
We no longer need that scroll view
to display it when it's not visible.
We can reuse it and move it in to
display another photo on the right.
So that's exactly what we're going to do.
We've got this set up that we just looked at.
Let's expand it again but now see only the
parts that we actually need at any given time.
So the shaded version of our paging scroll view
is the frame that's actually visible on the phone.
And so we only have one zooming scroll view
that's visible in that frame right now.
And so we only have to load one photo.
Now if we make it a little bigger so
that we can see this happen over time,
as the user pages through our photos, at
any point there's only going to be a maximum
of two different photos visible at any given time.
So we can page through and as we do it you
can see we only ever have two photos visible
and there is only ever two scroll
views created to show those photos.
So it's going to be much less memory and
much less set up cost initially in order
to even begin launching and displaying these things.
So where do we want to do this?
That's what we're trying to accomplish.
Well, we could do it in the layout
subviews method that Eliza just showed us.
But maybe we don't actually want to have to subclass
UIScrollView because it's really not even necessary
for the zooming case that we just looked at.
You could do that all without a subclass.
So if we didn't want to subclass, we
could instead implement the view--
scrollViewDidScroll delegate method which is called
under the same conditions as layout subviews.
Basically, every time that the user scrolls any amount
through the scroll view, either by dragging their finger
or by flicking or having it decelerate,
scrollViewDidScroll will be called
for every frame before that frame
is actually drawn on screen.
So you have a chance to add subviews if you're going to need
to display more content before that
empty spot even becomes visible.
So that's exactly what we'll do.
And Eliza is going to come back
up to show us how to do that.
>> Eliza Block: Alright, so we're going to just start
right where we left off with the same application.
I've moved back to the view controller header
file because we're going to need to add a couple
of new ibars in order to accomplish the tiling.
So our strategy is going to be-- we're going to
keep track of what tiles are currently visible.
So we're going to need a set to keep track of
the visible tiles and we're also going to--
as we pull out tiles that are already used because
they've gone off screen, we're going to keep track
of them in another set of recyclable tiles.
So I'm going to add two ibars here.
A recycled pages set and a visible pages set.
I'm also going to declare two new methods.
We are going to need a method will get us a
recycled page if there is one available so this is--
I've named this by analogy with the
UITableView dequeue reusable cell method.
So we'll get us a recycled page and we're also going
to need a method that actually accomplishes the tiling.
So let me switch over back to the implementation.
Alright, here's our load view method and at the bottom,
we have these lines of code that add all the pages in.
We don't want to do that because that's
what was using up all of our memory.
So I'm just going to delete that.
And instead what we'll do here is first we'll actually
create our recycled pages and visible pages sets
and then we'll just call tile pages once to get
the tiling started and that will have the effect
of showing the first page since that's the page
that you start on when this view is loaded.
Now as Josh has pointed out it's
not enough to tile the pages once.
We need to tile them every time
that the scroll view scrolls.
So for that purpose, I'm going to implement
this scrollViewDidScroll delegate method.
So we need to set our view controller as
the delegate of the paging scroll view.
And then we'll implement scrollViewDidScroll
and all we'll do is call tile pages
again every time the scroll view scrolls.
OK, so what does it look like to tile the pages?
I'm going to scroll down here to give us some space.
So the first thing that we need to do when
we're tiling the pages is to calculate how--
which pages should be visible given the
current content offset of our scroll view.
So for that purpose, what we're going to do
is grab the visible bounds of the scroll view.
And once we have the visible bounds, you can think of that
as a rectangle of the content and we're going to take a look
at that rectangle and you can think
of it as a bunch of columns of pixels.
So we'll look at the first column of pixels and we'll
see which page is that column of pixels associated with.
Which page is that column of pixels in?
And that's going to be the first page that we need to
display and then we'll look at the last column of pixels
and we'll find the page that that column is on.
And so that's going to be our range of pages.
That's the strategy.
So I'm going to paste some math here.
I don't want you to worry about it.
You can take a look at the sample code
and go through it and see how it works.
But what it's doing is, what I just described,
it's calculating the first needed page
index and the last needed page index.
Alright, so now that we know which pages
we need, let's first recycle the ones
that we don't need but that we already have in our view.
So for that, we're going to use
this visible pages set that we have.
And we're going to look at each
of the pages that's currently
in the visible pages set and find out is it needed or not.
Now, I've taken advantage of the
fact that I have a custom subclass
of UIScrollView that my pages are image scroll views.
And I've taught them to know what index they are.
So what page index they represent.
So what we're going to do is use that here.
We'll just ask the page that we're on, is
your index outside of our needed range?
Is it less than the first or greater
than the last needed page?
So if it is outside of the needed range, we're going
to recycle it by adding it to our recycled pages.
We're going to remove it from the Super View and we also
want to now take note of the fact that it's no longer
in our visible pages set so we want to
remove it from the visible pages set.
But don't do that in that way because adding this line
of code here would be mutating a
set while we're enumerating it.
And that's a really bad idea once you've checked that bug
into Springboard you never make the same mistake again.
[Laughter] So let's take that out and after the
four loops safely, when we're finished enumerating,
we can take advantage of the fact that these
are sets and we can do a set subtraction
and just remove all those recycled pages from the visible
pages and that's a safer way to handle that problem.
So now that we've recycled our pages, we need to add the
ones that are needed, that aren't already in the view.
So for that, we're going to iterate through from the
first to the last needed page and we'll ask is this--
do we actually have a page that's indexed already?
And I've made a convenience method here,
isDisplayingPageForIndex.
What that does is it actually just looks through the
visible pages and sees whether there is one at that index.
So if there's not, so if we're missing this
page that we need, we're going to make the page,
configure it for the right index, add it as a
subview of our scroll view just like we did before.
And finally, we'll note that this
page is now in our visible set.
OK, we're almost ready to go.
This would work at least at first.
But you'll notice that I'm actually not using my
recycled pages at all for every time that we discover
that we need a new page, I'm creating one from scratch.
So eventually, we would hit our same memory problem as
we had in the first version once we filled up-- had--
you know, scrolled all the way over to the last page.
So let's actually use the recycled pages here.
We need to implement this dequeue
recycled page method that I declared.
So here it is.
I grab any object out of my recycled pages set
and I do a little memory management stuff here,
because presumably the recycled pages
set owns the last retain on this page.
So if I were to remove it from the
recycled pages set and then return it,
it would actually go away before
it had a chance to be returned.
So first, I retain and auto release it to guarantee
that that page is still there when we go try to use it.
Alright, so scrolling back up to here, instead of creating a
page at this point, we're going to try to get a recycled one
by calling that method and only if we fail to get one.
So if there wasn't one available,
will we actually create a new one.
Oops. Alright, so let me go ahead and build
this version and we'll see what happens.
Alright, so I've got my same app.
It looks the same.
I can zoom in just as I could before.
I can scroll to the next page.
So, things seem OK.
Let's take a look at the activity monitor and
see how much memory we're using this time.
There it is.
Just a little over a 100 megabytes.
So we've cut our memory consumption
down by a significant amount.
Enough that you could probably actually run
this version of this application on the device.
But let me just signal a problem.
I don't know if it was very apparent
when I was scrolling through here.
But if you take a look at the arrow, as I scroll
there's a perceptible lag before the next page appears.
And the experience of it, you really feel
like this thing isn't responding very well.
The reason is that, I'm actually having to load
and decompress that huge image at the beginning
of the scroll, as I'm getting to my next page.
And so, you actually get a bad performance problem
on this really powerful Mac, it's perceptible.
On the device it would be really unacceptable.
So let's-- I'm going to turn you back over to
Josh who will tell us how we can fix that problem.
[ Applause ]
>> Josh Shaffer: Alright, thanks again Eliza.
So, we're getting much closer.
We're actually almost there.
Now I'm sure this is familiar.
You've got the maps application from
iPhone OS here and it shows you exactly
where you are at, you know, any given time.
And at some point later, it shows
you what else is where you are.
The way that works is by using CATiled layer to
lazily load all of the different tiles that make
up the map that you're currently viewing.
And these tiles are loaded off the network on a background
thread and then get drawn when they are finally available.
And see, a tiled layer is available for
use as public API in your own applications.
So you can do the exact same thing.
So we already looked at this first subview tiling example.
And that's a great way to do tiling if you
need to tile more complicated view hierarchies.
Things like whole table view cells that
are composed of multiple subviews and,
you know, UILabels, and all these other things.
Or in the case of our paging, we
needed to tile entire UIScrollViews.
So if you need complicated view hierarchies, you
really want to do your own subview tiling like that.
But if you can do your drawing in drawRect, then
CATiledLayer can make this significantly easier.
It can remove almost all of the management code that we just
looked at and, basically, just leave you do draw your content
when you're asked and handles things like caching
to make sure that it's only using the amount
of memory that's reasonable given
what the user has looked at.
It only asks you to draw things that are currently visible.
It supports multiple zoom scales.
So as your user pinches to zoom in and out, it will
ask you for new tiles at a different zoom scale based
on how many different levels of detail you'd like to
be able to display. And it manages all these for you.
It's very easy to configure and you don't
have to write any code to implement all that.
So let's take a look at that guy.
The idea here is that we're going to
start out at a 100 percent zoom scale
and will assume that our tiles are 100 by 100.
So obviously, that means that in this case for
this portion of this frog we're looking at,
we've got 16 tiles to make up this
entire portion of the frog.
And that total size is 400 by 400.
So assuming this was all we were looking at, we would
have created a CATiledLayer with bounds of 400 by 400
and we'd add that as a subview of our zooming scroll view.
Now as the user pinches in, we're going to drop
down to the 50 percent zoom scale at some point
because the user will have pinched enough
that we're now only viewing half as much
or we're viewing the image at half the original size.
Once that's happened, assuming you've configured
your tiled layer to support a second level of detail,
you'll be asked to draw the content again.
But now, you'll be asked to draw it
at 50 percent of its original size.
When that happens, the tiles will still be 100 by 100.
So that's going to remain the same for the entire
duration of our example here and we're going to try
and draw the exact same portion of our original image.
So in that same frog we just saw before, we're
drawing again, but now, using tiles that are only as--
a quarter as big, half an inch dimension.
What this is going to end up doing though is asking us
to draw it into a bounds that is still the original size
because if you looked at CALayer's geometry or UIView
geometry, you know that when a transform is applied
to a view, that modifies the view's
frame which is the view size
and the view super view, but it
doesn't modify the view's bounds.
Frame is computed from bounds and
transform among a couple other properties.
So the bounds of that CATiledLayer is still 400 by 400 and
we're going to be asked to draw under those same bounds.
So effectively, we're going to be asked to draw
this, 100 by 100 tiles into a 400 by 400 bounds.
Now, it may seem like-- when you start thinking about this,
you're going to have to multiply by your scale and so,
you'll draw your 100 by 100 tile and stretch
it to be 200 by 200, and that's true.
But that won't actually end up causing any actual stretching
in the drawing because the context you'll be asked to draw
in actually has the inverse scale on it that
will scale it back down by half the amount.
So you'll stretch it out to 200 by 200 and CG when it goes
to draw it will then scale it back down to 100 by 100.
And the final draw of that image
will just be a 1 to 1 pixel bit.
So the backing store for this is actually 200 by 200.
So you saved the memory and you don't
actually lose image quality by scaling.
Now similarly, if we had another level of
detail below this, we could have 25 percent.
Once we got there, we'd be asked to draw the entire image
that we the just looked at into a single 100 by 100 tile
because we now have 1/16 the amount of pixels, 1/16
the amount of memory being used, we've saved a lot
and the user has pinched down so far that it doesn't
matter that we don't have all those original pixels
because they're not able to be drawn on screen anyway.
Again, 100 by 100, we're going to stretch
it when we draw it to fill that 400 by 400.
Now as I said, CA or Core Graphics rather, when
it actually does this drawing is really going
to be drawing it into that 100 by 100 backing store.
So that really is what you have
in memory, just a smaller amount.
So at that point, assuming the user has
actually zoomed through all these things,
Core Animation now has three different
sets of tiles to work with.
And as the user pinches to zoom in and out or
double taps if you've got programmatic zooming,
Core Animation will correctly swap
between which tile is necessary
to optimally display whatever zoom
scale the user's currently looking at.
And it's even cooler because if you do some
programmatic animations for the zoom scale changes,
Core Animation does trilinear filtering to blend these
as they're animating between them and it's really,
really cool and it's stuff that we'd take a long
time to actually implement on your own from scratch
and you get it all for free just by using CATiledLayer.
Alright, so hopefully that, you know,
kind of makes you want to use it.
What is it that we actually have to do to use it?
Well, turns out it's actually really easy.
There are just two methods on UIView that you
have to implement to begin using a CATiledLayer.
First off, you actually have to tell UIKit that
you want to use CATiledLayer, because by default,
when a UIView is created, that UIView is
backed by a plain CALayer, not a subclass.
So in order to change to a CATiledLayer, you implement a
class method in your UIView subclass called "layer class."
And you just return CATiledLayer class from that method.
Once you've done that, that's it.
UIKit is creating a CATiledLayer
for you to back your UIView.
So all that's left to do is actually draw
your content and you do that in drawRect,
the same as you would with any other
layer that you were trying to draw.
Now you need two pieces of information in order
to accurately draw what you're being asked.
The first is the rect that comprises the tiles
you actually are being asked to draw right now.
And the second is the zoom scale
that you're being asked to draw at.
So the rect is easy.
We've got it right there, right?
And as we already said, keep in mind, this rect
is in the original bounds of that CATiledLayer
because the bounds don't change while you're zooming.
So how do we get the zoom scale though, there's no
property or no perimeter telling us what that is?
Well, this is a little tricky.
You actually have to pull the zoom
scale out of the current graphics--
no, the current transform matrix of the current
graphics context associated with this drawRect.
So what does that mean?
Well, you can call UIGraphicsGetCurrentContext to
find out which context you're being asked to draw into
and that'll be a CGContextRef that you're actually
going to draw into using core graphics calls.
Then from that, you can get the current transform matrix
by calling CGContextGetCTM on the context we just got.
And that's going to return the
CGAffineTransform which is the transform
that is applied to all drawings done in that context.
This is the bit I talked about where even
though you stretch out by multiplying
by 2 this affine transform is going to scale back down by 2.
So you'll end up actually not stretching.
And in fact, that's exactly what we're looking for to
figure out which level of detail we're being asked to draw.
We need to figure out what that scale
is that's on the CGAffineTransform.
Now, I already know because we're using a
UIScrollView that were being scaled uniformly
in the horizontal and vertical dimension.
So we can greatly simplify the act of figuring out what
our scale is because we know that we can only zoom in--
or that-- we can look at either because they're the same.
And we know that there's no rotation on this transform.
If there was, you'd have to do a bit more math.
So this transform will assume just as the
scale that's applied by the UIScrollView.
Given that, we can pull the scale out of the .a component--
or .a field of this CGAffineTransform .a and .d are the two
scale fields, not too very important what that is right now.
But we'll just get it out.
So transform.a is the CGFloat that represents
the scale we're being asked to draw it.
And that's it.
We now have the rect we're being asked to draw.
Make sure that you only draw that rect or
else you've negated the entire, you know,
benefit of doing this because it's
only asking you to draw what's visible
on screen and we also know what scale to draw in.
So we can now draw our tiles.
Now, I see some of you guys staring at me
and saying, "I tried this, didn't work."
Well, that was actually true in iPhone OS 3.0.
There was a little bit more that you
had to do in order to make that work.
I've got a link to the tech note up here.
If you want to deploy on iPhone OS 3.0, you can
check out this tech note at developer.apple.com.
If you can't write it all down right now, you can
just search developer.apple.com for CATiledLayer.
There're not a lot of references
so you can find it pretty quickly.
But we'll ignore that for now.
In iPhone OS or iOS 4, UIGraphicsGetCurrentContext as you
heard in the session earlier today is now thread safe.
So even though CATiledLayer is going to call drawRect on
the background thread, that's now thread safe and you--
you're UIGraphicsGetCurrentContext call will not be trampled
by another drawRect happening on
your main thread simultaneously.
Each one will have correctly their own current context.
Also, UI image, UIColor, UIFont and the NSString
drawing additions in UIKit, they're also threadsafe now.
So there's actually a lot of UIKit based
drawing that you can do in this drawRect even
on your background thread that
CATiledLayer will call you on.
So with that, let's have Eliza come back up and
want-- and make one final modification to her demo.
[ Applause ]
>> Eliza Block: Alright.
So once again, we're going to start where we left off.
We're going to modify this so that our zooming
scroll views instead of using image views as--
in order to display an image, are going to use
a subclass of UIView that we're going to write
in a moment and I called it a tiling view.
Alright so before we get to the implementation of the
tiling view, there's one modification that we need to make
to prepare to use tiles instead of full images.
And that is-- this was a piece of
the demo that I didn't show you.
But a part of configuring the zooming scroll view was
setting its content size for the zooming scroll view
which we were setting to be the size
of the image that we were displaying.
And conveniently, since we were displaying an entire
image, we could just grab the size right out of that image
and use that to be the content
size of our zooming scroll view.
Now, we don't want to open an entire image because
that was what was taking so long and making it--
or delay when we were trying to page from page to page.
So instead, we're going to not open the entire image
which means that in your own app, you would need some way
to have access up front to the sizes of
the content that you're going to display.
So I'm going to show you where
this change needs to take place.
It's in this configurePage-forIndex method
that I keep calling, but I haven't shown you yet.
So, let's scroll down to that method now.
Here it is.
It doesn't do much.
It sets the index of the page which we used
if you remember for the subview tiling.
It sets the frame for the page and it tells
the page to display an image which it looks
up with this convenience imageAtIndex method.
So this is the line we need to get rid of because we
don't want to be opening that whole image anymore instead,
we're going to tell our page to display a tiled image
named something and it's going to use this name to figure
out which tiles to load and we're also going to tell it
the size which I've added as metadata in this project
because it's going to need to set the content size
on the scroll view and it's also going to need
to correctly size our tiling view using that size.
OK, so now with that done, we can switch over to
this tiling view implementation and we're going
to do the steps that Josh already told you about.
So first, we need to override the layer class method
to return a CATiledLayer so that this view is now going
to be backed by a special CATiledLayer
rather than a regular CALayer.
We also need to tell our view-- rather, tell the
TiledLayer in question how many levels of detail to display.
So for that, I'm going to override initWithFrame.
And in initWithFrame, I'm now going to grab the tiled layer
by asking for my own views layer property and I'm going
to set the levels of-- the levels of detail to 4.
Now what that means, is we're going to be asked in our
drawRect to draw at potentially four different scales.
Each one is half the previous ones.
So that the maximum scale is going to be
100 percent then we're going to get asked
for 50 percent, 25 percent, and 12-1/2 percent tiles.
And in fact, these images are so large
that you'll see that we only are going
to need the 12-1/2 percent tiles for
quite a while as we first view them.
So what is the drawRect look like?
Alright so the first thing we're going to do is figure out
what is the scale that were currently being asked to draw at
and I'm going to do that the way that Josh explained.
We're going to get our current graphics context and
we're going to grab the scale out of that current context
by getting the current transform matrix and
asking that transform for its A component.
Alright, so now, we have our scale.
We're also going to need-- in order to
figure out which tiles we need to draw
in order to fill the rect that we've been passed.
We're going to need to know how big the tiles are.
And that's a property on CATiledLayer.
So I will get my CATiledLayer and
I'll ask it for its tile size.
Alright, so now comes the part that's perhaps the weirdest.
It's not going to be good enough to use this tile size
as is because if we we're being asked to draw a scaled
down version of our tiles, we need to adjust for the--
for the scale when we think about how big our tiles are.
And the reason for that is what Josh explained
but I'll just talk about it briefly again.
If we're being asked to draw at, say, the 50
percent scale, we still need to stretch those tiles
out to fill the entire region of the original image.
So although our tiles have less information in
them, we're going to stretch them out to be bigger
than they really are to fill that full region.
So we need to compensate for our
scale by adjusting our tile size.
And we're going to do that by dividing
both the width and the height by the scale.
So we're going to pretend that our tile size is
bigger as we get to smaller and smaller scales.
So, alright, now that we've got our adjusted tile size, we
need to first figure out which of these tiles do we need
in order to fill this rectangle that we've been passed.
So this is again the same math that I did before.
We're going to look at the rectangle that we need and we're
going to look at the top and bottom and left and right rows
and columns of pixels and we're going to figure out which
tiles those are associated with and then we're going
to iterate through the rows that we need
and draw all of the tiles that we need.
So block of math calculating the
first column that we need of tiles
and the last column and the first row and the last row.
And now, we're just going to-- for each tile that
we need-- so for each row and within each row,
for each column, we're going to draw that tile.
OK, so I've got a convenience method
which will grab us the tile.
What it does is it just looks at the scale, the row, and
the column and I've got a naming convention for my tiles
that are saved as images here that will
grab us the right one for that purpose.
Next, we need to calculate the rectangle that
we're going to use to draw this tile into.
So the origin of the rectangle is just going to be the
column times the width of our adjusted tile size by the row
that we're on times the height of the tile size.
So we're going to move over and down by the appropriate
amount given what column and row that we're on.
And the size of our rectangle is going
to just be the size of our tiles,
but adjusted once again for the scale that we're drawing at.
Alright, there's one caveat.
This is going to cause some problems.
Let me just show you a couple of these tiles.
So, most of the tiles-- here's a
good example of our 12-1/2 percent.
Here's a piece of a frog taking up a full tile.
But the tile underneath this is actually only
a partial tile and the reason for that is just
that my image is very unlikely to be
an exact multiple of my tile size.
So you're always going to have at the bottom
and at the right edge some partial tiles
that you don't want to stretch out.
So if I were to draw going back to the code here.
If I were to draw that partial tile
into the rectangle that I just computed,
it would be stretched to fill the
entire square and it would look weird.
It would look like your image was
kind of bleeding off at the edges.
So we need to compensate for that.
And we're going to do this by checking--
we kind of want to check,
is the rectangle that we just computed,
is it going off of our bounds?
Is it outside of our bounds?
And if it is, then we need to truncate
it so that it stays within our bounds.
And we can actually do that with one line of code just
by taking the rectangle intersection of our bounds
and the tile rect that we're about to draw.
So then we can just go ahead.
Draw our tile and this is all you would really need to do.
For demonstration purposes I'm going to add some
white lines over the tiles so that when I build this,
you'll be able to see when we change
from one tile size to the next.
So there are just a couple of lines that draw
white border around the tile that we just drew.
OK, so let me just go ahead and run this.
Alright, so we've got-- what we're
seeing here is the 12-1/2 percent scale.
And you can see that you only need a total of actually four
tiles in order to draw the entire 12-1/2 percent image.
As I zoom in, it razzed up to 25 percent and then as
I continue to zoom in, we get the 50 percent tiles,
and finally we-- the 100 percent
tiles, look how big these images are.
So here's the level of detail that we've got.
You can see that at a 100 percent, this image
is huge and if you can imagine the entire image,
how many of these 100 percent tiles are needed
to make-- to construct the entire image?
That was what we were loading in the
previous version before we started tiling.
We were loading all of the 100 percent
tiles in the form of one image.
And then we were scaling that down to fit
the screen which was extraordinarily wasteful
and that's why we were using so much memory.
So let me just test this by scrolling around.
I can scroll really fast.
I can zoom in again and my center
ring still works even if I zoom out.
So how much memory are we using now?
Switch back to the activity monitor
and I'll just zoom in on that.
A total of 16 megabytes of real memory [background
applause] even though I've zoomed in and out a ton.
[Applause] Great. And that's all there is to it, so back to Josh.
[ Applause ]
>> Josh Shaffer: I got it.
Alright, thanks Eliza.
So that's pretty much there is to it, right?
[Laughter] Simple, it's five lines of code.
>> Josh Shaffer: Sample code is
available either now or soon after.
It's already been gone through and will be up on the web,
available for download and also associated with the session
through the developer-- WWDC, attendee site.
If you have any other questions, Bill Dudney
is the Application Frameworks Evangelist.
It's a long URL but UIScrollView has a whole class
reference that has all kinds of additional information
about scroll view, and of course,
the Apple Developer Forums.
There are a couple of related sessions later this week.
If you're interested in how the photos
application does detection of taps and double taps
and two-finger taps to do zooming in and out on images.
There's a whole new framework for
doing it in iPhone OS 3.2 and 4.0.
And that's the-- We're going to talk about in a Simplifying
Touch Event Handling with Gesture Recognizer session.
And also, if you want more information about table views
and how you can display just those
vertically scrolling bits of content.
The Mastering Table View session is on Thursday at 11:30.
Both of those are here.
The Gesture Recognizer is tomorrow at 3:15.
So that's about it.
The only thing I'd like to say is, you
really don't have to write your own.
You don't have to go looking for third party frameworks.
You don't have to start from scratch with UIView.
That's all.
So, thanks a lot.