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.
So, thanks a lot.