Configure player

Close

WWDC Index does not host video files

If you have access to video files, you can configure a URL pattern to be used in a video player.

URL pattern

preview

Use any of these variables in your URL pattern, the pattern is stored in your browsers' local storage.

$id
ID of session: wwdc2009-102
$eventId
ID of event: wwdc2009
$eventContentId
ID of session without event part: 102
$eventShortId
Shortened ID of event: wwdc09
$year
Year of session: 2009
$extension
Extension of original filename: m4v
$filenameAlmostEvery
Filename from "(Almost) Every..." gist: [2009] [Session 102] Mastering i...

WWDC09 • Session 102

Mastering iPhone Scroll Views

iPhone • 55:22

Scrollable content can be found in nearly every iPhone application. Find out how to do it right. Learn to process events in subviews and combine horizontal paging with vertical scrolling. Understand the best practices for zooming, scaling, and tiling inside your scroll views. This session gives you everything you need to produce efficient, high quality scroll views in your iPhone app.

Speakers: Josh Shaffer, Eliza Block

Unlisted on Apple Developer site

Downloads from Apple

SD Video (169 MB)

Transcript

This transcript has potential transcription errors. We are working on an improved version.

Good afternoon, my name's Josh and I'm an engineer on the iPhone Frameworks Team. And I'll be joined shortly by Eliza, who's an engineer on the iPhone Apps Team and she'll be doing some great demos of all kinds of things that you can do in UIScrollView. And all the demos that she's doing actually are going to be available as sample code, in fact, I believe they're already up.

So...

[ Laughter ]

[ Applause ]

...you'll actually want to give her many hands over the course of this time. She's spent a lot of time on these demos. There's a lot of great stuff in there. I think it's going to help everyone out, hopefully. So do check that out. So we're going to talk about UIScrollView.

When you have a lot of content and you're starting out, trying to figure out what to do with it, you're looking through the docs, seeing what you've got and you like it. The first thing you're probably going to come across is UIScrollView, and that's great. And it may be exactly what you want.

But you may also be interested in looking at some of the other options that are available. If you missed the session yesterday, there was a whole session on UITableView, you can check that out later. There's also UITextView, if you have just a lot of text, and UIWebView, if you've got HTML content or certain other types of documentsts.

You can display it in those. Anybody not need to deal with UIScrollView directly, but for the purposes of this session, we're going to assume that that's not what you want and you need to display different content and do it with UIScrollView itself. So you might need to do this if you've got more content that will fit on any one screen, obviously, or more content than you can fit in memory at any one time. So we're going to go over a few things today. We'll start out with configuring your UIScrollView.

We'll talk about how you can get the default scrolling and zooming behaviors. This is pretty basic stuff. We're not going to spend a ton of time on it, but I want to make sure that we're all on the same page on how this works. There's been a bit of confusion about some of the properties on ScrollView and some things that are not necessarily incredibly obvious, just from looking at the headers in the docs, right off the bat. So we'll talk about that.

Then we're going to talk about interaction. You've got touches that you want to handle in subviews of your ScrollView and you may or may not want them to scroll the ScrollView, depending on what the user's doing or what they're touching. We'll talk about how you can control that. And finally, we'll talk about optimizing your content. You've got a lot of content that you want to display in your ScrollView and maybe it doesn't all fit in memory, maybe it doesn't even all fit on the device, there's limited flash available.

We'll talk about what you can do to optimize the display, speed up load times, everything to make the content display efficiently for your users. So first off, let's take a look at a few examples, real quickly, of what UIScrollView is used for on iPhone OS already. So first off, on the left here, we've got a UITableView, actually, which is used to display the bookmarks in Safari. And this is a pretty simple example.

It's just some text, a textual list with some icons on the left. If you missed the session, you can check it out. I'm sure a lot of you have already worked with UITableViews. You can do some pretty complicated UIs just using UITableView. But now if we take a look at something a little more complicated, we've got the Maps application.

This is also a UIScrollView, although it's actually basically letting your user pan over a nearly infinite plane of data and not-- that data is not even stored on the phone. It's pulled down over the network from the Google servers. And UIScrollView, it's just a subclass of that, that does some tiling in order to get this content displayed. And we'll talk about tiling content later on and we've got a big, good demo of that as well.

And finally, we've got the stocks application which in iPhone OS 3.0 now has even more ScrollViews in it. So at the top you've got a table view, which is just some customized table view cells to display all the individual stocks. And then at the bottom, there's a horizontal paging ScrollView that has an embedded vertically scrolling UITableView on one of the pages.

Now over the course of the last year, we've heard from a lot of people that they'd like to do embedded ScrollViews, one inside of another. And the answer has thus far pretty much been, Yeah that's either not possible or incredibly difficult to do right. So in iPhone OS 3.0 embedding one ScrollView within another is now supported.

And we'll talk about that later on as well.

[ Applause ]

So first off, configuring your ScrollViews. So you've got some content and you want to allow your users to scroll around in it. So what are we going to do? First off, we've got our content obviously. We have to have that. And in this case we've got one big UI view with a bunch of images that are subviews in it, and we're going to set it up so that just right off the bat users can scroll.

And there's actually only one thing you need to do in order to get that working. After-- while-- after you've added this content as a subview of your ScrollView, one thing to do, and that's set the content size property. And content size is just the width and height of your content. And with that one thing set, that tells the ScrollView how much to scroll over, and right off the bat users can pan around within your content.

Now you might also want to add some extra padding around the edges of this, maybe at the top and bottom or the left and right. And we'll talk about why in just a second. But there's a property that lets you do that and that's the content inset. And that basically just defines some-- an extra amount of area around your content that can be scrolled into the ScrollView's frame.

So if we set some content inset at the top and bottom, that basically just adds some padding above and below the content. So it, it's a little easier to understand why you'd want to do this if we actually put this in an iPhone and take a look at it.

So let's overlay an iPhone on top here and now we've got a full screen UIScrollView. It's taking up the entire screen to maximize the amount of space that the user can use to look at your content. But you probably also want some kind of controls to be visible at some point.

So we're going to add at the top a navigation bar and a status bar. But now the content's behind that and you know that's not so good. You really want your users to be able to see all the content even when it's scrolled all the way down. So setting content inset on the top will give them some extra padding that they can scroll down in to allow the content to flow below these other user interface elements.

But allow them to scroll it up so it's still visible behind the transparent elements. And we can do the same thing on the bottom with the toolbar. And just set content inset on the bottom, which gives some more area on the bottom that can be scrolled up. Now this can also be useful in cases like when you get a notification that the keyboard has become visible on screen. It's a common first choice to think that you need to actually resize your ScrollView in order to push it up above the keyboard.

But if you actually just set a content inset on the bottom, you don't need to change the ScrollView's frame at all. It actually animates better and there's-- you tend to have less glitches during the animation of the keyboard coming up. And it's easier to synchronize with the keyboard.

So you can just animatedly set a content inset on the bottom and that'll get some extra space for the content to scroll up above the keyboard so it stays visible. If you use UITableView Controller, it actually does this for you if the subview of a TableView is a text field.

So now we've got our content and it's fully visible, it's scrolled, can scroll underneath and above our User Interface elements. But there's one other piece that's left. So there are no-- there are no scroll controls visible in a UIScrollView. The entire ScrollView itself is the-- is the control.

These are just-- moves their finger in it. But when the user is scrolling, there are scroll indicators on the right side and the bottom. And right now as things stand, even though we've set our content inset to allow the content to be below it, the scroll indicators, they still take up at most the entire ScrollView frame, vertically and horizontally. So we really want that to get pushed down to never be underneath these controls either. And that's what the scroll indicator insets property is for. So that just defines the starting point in your ScrollView's frame that the scroll indicator will go at.

So it'll never go above that point. So we'll set the scroll indicator inset on the top there and that'll push the maximum scroll indicator down. So even when the content is fully scrolled down, the scroll indicator will be below our UI at the top of the screen. So now we've got our content and we-- the user can scroll it. They've scrolled it up above part of our iPhone so it's sticking out the top there, that really does happen.

But at some point in your application you may want to know programmatically where the user is at in their content. And that's what the content offset property does for you. Now the content offset is the point in your content that's currently visible at the top left of the ScrollView's frame.

This can be a little confusing at first and it's not entirely obvious, but that's basically what it is. If you saw Dan's session yesterday on iPhone Views and Animations, he talked about the difference between bounds and frame on a view. This is just kind of a quick aside, but content offset is actually just the bounds origin of the ScrollView's frame.

If you didn't, he always-- Dan kind of mentioned that the bounds origin is always 00, and generally that's true. But bounds is just actually a view into some part of that-- sorry, a window into some part of the view. So in order to scroll, ScrollView just changes the bounds origin, which is also the content offset. So anyway, in this case here, we've only got vertically scrolling content, nothing horizontal.

So we only have a Y component of our content offset. And as you can see there, that is the point in the content visible at the top left of the ScrollView's frame. So let's put all this together and look at it in a bigger example here. We've got some content, it's a big-- just a big image and we've got content inset set to give us some extra borders around the edge. Now if we overlay our ScrollView frame on top of that just by graying out the part that's not visible now, we can take a look at content offset.

And as I just said, that's the point in the content currently visible at the top left of the ScrollView's frame. Now if we scroll that content so that it's actually inset as far as it can possibly scroll to the left and the top, we've now got our content insets visible in the frame as well.

So at this point there is no point in, in the content itself visible at the top left of the ScrollView frame. So an important detail here is that at that, at that time the content offset is actually negative. So you should be prepared to expect that at some points if you're using content inset, a negative content offset may be valid. And in this case, we're scrolled all the way as far as we can so the content offset is just the negative content inset left and negative content inset top.

All right, so now we've got our scrolling set up, users can scroll around in our content and pan and that's great. But ScrollView also provides support for zooming in on that content. And with all this already done, there's actually only two things that you need to do to add support for zooming in your ScrollView.

So there's two properties and you'll need to change at least one of them. And one delegate method that you'll have to implement. So the properties are minimum and maximum zoom scale. Now, by default both of these are 1, so that defines the range over which the user can zoom your content. So by default there is no range.

They're always at the 1.0 zoom scale. So in order to enable zooming, you have to at least set the minimum to less than 1 or the maximum to greater than 1 or some other combination that defines some sort of range over which the user can zoom. And then once that's set, there's one last thing. You have to implement the view for zooming and ScrollView delegate method.

And this-- with this method you'll just return some subview of your ScrollView that you'd like to be zoomed when the user pinches. Now when the ScrollView zooms this view, it just sets a transform on that view. So the view has a transform property and ScrollView sets the transform on there to transform appropriate for the current zoom scale.

Additionally it also changes the content size property on the ScrollView itself to compensate for the fact that the content size having been zoomed is now either larger or smaller. So over the course of the last year, we've also been hearing that you guys would like to be able to programmatically set and get the zoom scale. And so in iPhone OS 3.0, we've added one new property and two new methods to programmatically get and set this.

[ Applause ]

[ Laughter ]

Thank you.

So the, the property is just the zoom scale property and it's a floating point value that is the current zoom scale within the minimum and maximum range that you've defined. And you can both set and get that property. If you want to add animation, then there's actually these two new methods that we're going to talk about.

The first is set zoomScaleAnimated. And this just takes a floating-point value and a Boolean animated property and it will zoom in and out on the center point of what's currently visible within a ScrollView's frame right now. So as you can see here, as we set the zoomScaleAnimated, we're just zooming around the center of what we can currently see. But you might want some more control over what becomes visible as a result of this zoom. And for that we've added a second method, which is the zoomToRectAnimated.

So with this you can specify some rectangle in your content that will be zoomed to best fit within the ScrollView's frame. So if you specify this rectangle around the dragon's mouth here, and then call zoomToRectAnimated, we're going to zoom that up to fit as best we can within the ScrollView's frame, taking into account the minimum and maximum zoom scale.

So we won't let you scroll outside or zoom rather outside of the bounds that you've told us that you want zooming within. Similarly if you've got larger content here and you specify a rectangle that's bigger, we'll zoom down in order to fit that in as best we can. So there's one last thing I want to talk about that's not really related to this before I ask Eliza to come up to do a demo.

But it's also new in 3.0 so we're going to mention it real quick, and that is the new deceleration rate property on UIScrollView. So this controls how quickly the content comes to rest after your user flicks the scroll within the ScrollView. So by default we've got the UIScrollView deceleration rate normal.

And this is the same as the behavior in iPhone OS 2.0. So on the left you can see the user scrolls and it actually takes a while for that content to slow down and come to rest. So on iPhone OS 3.0 now you can also specify UIScrollView deceleration rate fast.

And that will cause the ScrollView to come to rest much more quickly. So as you can see here, the user flicks to scroll and the, the content comes to rest almost immediately. So with that, I'd like to ask Eliza to come up and give us a demo of some of the programmatic zooming features in iPhone OS 3.0.

[ Applause ]

Thanks. Hi I'm Eliza and I'm an engineer on the iPhone Applications team. I'm going to show you how to use some of the new zooming API that we've just added to UIScrollView in OS 3.0. So what you see here is a really simple application. It has a single view controller, which is managing a UIScrollView. And you can't do much with this yet. I'll show you what you can do. We can scroll around in the content.

And we can pinch to zoom in and out. So what I'd like to do in this demo is add two features to this application. First we're going to arrange it so that when the application launches, we zoom all the way out on the image so that the entire content is visible. Second we're going to add sensitivity to taps for zooming in and out on the content much like in the Maps application.

So let me show you the code.

[ Period of silence ]

Almost all of the work here is being done in the load view method of our root view controller. So we create our ScrollView, we give it some content in the form of a UIImageView. We set the content size property as Josh mentioned.

And then I'm setting some properties here that allow us to do the scrolling by pinch-- the zooming rather, by pinching. In particular the minimum zoom scale property. And finally we have implemented the one delegate method that's required to allow the pinching to zoom and in that method we return the image view that we created. So if we want to change this so that when it launches, you see the entire image, we actually only need to add a single line of code.

We're going to use the new zoom scale property that's-- that we've added to UIScrollView. And here I've calculated a minimum scale so that the image will fit perfectly width wise in the frame of the ScrollView. So I'm just going to set the zoom scale to be that size and I'm going to build it again. And let's see what happens.

So now when it launches the entire image is visible. OK, so next let's add sensitivity to taps. So the Maps application, when you double-tap, you zoom in by one step. And when you use a two-fingered tap, you zoom out. So we need to have a way to detect the taps and UIImageView doesn't actually know how to detect taps.

So what I've done is, I've added a subclass of UIImageView called a tap detecting image view, which has built into it the ability to listen for taps. So I'm going to replace our image view by a tap detecting image view. And then the tap detecting image view sends a message to its delegate when it detects a tap. So we'll set our root view controller to be the delegate of the tap detecting image view.

[ Period of silence ]

All right. Last thing we need to do is implement the delegate methods that are going to actually cause the zooming to take place. So right here I'll make a section for our delegate methods. The first one we're going to implement is the-- gets called when a double-tap occurs. And in response to a double-tap, we're going to zoom in on the content. We'll also implement the, the method that will be called when a two-fingered tap occurs.

And in response to that we'll zoom out. All right, how do we figure out what new scale we're going to zoom to? We'll need to-- we're going to calculate the scale we want and again we're going to use the zoom scale property in order to get the current scale that we're zoomed to at the moment. So and then we'll multiply that by some constant, say 1.5. We'll do the same thing in the case of the zooming out, but here we'll divide by 1.5.

And all that remains is to tell the ScrollView to change its scale to that new value. So the simplest way to do this is to use the set zoomScaleAnimated method rather that Josh just mentioned. So let's do that. We'll set the zoom scale to our new scale and we'll animate the transition.

And now I'm going to build this again and see how it works.

[ Period of silence ]

OK. So I'm going to tap on the image and as you can see as I double-tap, we get zooming and as I two fingered tap, we get zooming back out. There's still something a little wrong with this, which is that we're ignoring the location of my tap. So I'm tapping all over the image but we always zoom in around the center point, and that's because we're using the set zoomScaleAnimated method and that method zooms in in such a way as to preserve the center point in the visible region.

If we want to make it so that it zooms around the point where the user tapped, we're going to need to use a method that gives us more control over what region of the image becomes visible as the zooming occurs. So the zoomToRectAnimated method is the best way to do that. So let's change this code to use the zoomToRectAnimated method instead.

First we're going to need to calculate the rectangle that we want. And so we're going to want to figure out a rectangle in our image that perfectly centers the top point which has been passed to us by our, by our image view. I've written a utility method to do this, which I'm going to show you here. It just calculates the zoom rect in the image for a particular scale with a particular center point.

One thing to note about this, don't worry about understanding exactly how it works right now. But one thing to notice about it is that if we want the maximum control over exactly what region of the image becomes visible after the zooming, you want to choose a rectangle that is the same aspect ratio as the ScrollView itself. If you choose a rectangle that's a different shape, we'll make it so that whole rectangle is visible in the view, but we might put it at the top or the bottom or the middle, depending on where you're currently scrolled to.

So what I've done is calculated a rectangle that's the same shape as the ScrollView and that has that center point that we want. So we'll call that method here. And then we'll ask the ScrollView to zoom to that rectangle and to animate the transition. We'll do the same thing in the zooming out case.

[ Period of silence ]

And let's go ahead and build.

[ Period of silence ]

All right, this time when I double-tap here, I'll double-tap on her, I don't know if the mouse is visible. I'm double-tapping on her badge on her collar. Now I'm double-tapping on her nose. And then again on her badge.

So as you can see, we're actually zooming right around the point of the double-tap. Thanks.

[ Applause ]

Thanks Eliza. So now you've got your content in your ScrollView and you've got your default interaction behaviors. Your users can zoom around, zoom in on it, they can pan around on it.

Everything's working great. Now you might want to add some custom interactions with subviews of your ScrollView. So we'll talk about now how you can determine what view gets to handle the touches that are in the ScrollView. So this has been an area that's had some confusion over the last year.

So hopefully we will clarify some of that with the two properties and two methods that are provided on UIScrollView to give you some control over how this works. So the first property we'll talk about is Delays Content Touches. Now the default value here is Yes, so everything we're going to say is the default behavior that you get out of the box unless you change this.

So normally when a finger comes down in a subview of a ScrollView, touchesBegan is not delivered to that subview until after a brief delay. Now the idea here is that as a result of receiving this touch, you're going to provide some visual feedback that the touch has come down.

We don't want to do that if the user is immediately going to begin scrolling. And we don't really know when the finger first comes down whether it's going to be an attempt to touch something in the ScrollView or an attempt to scroll. So by delaying this delivery, we give the user a chance to start scrolling and not see a quick flash of content. So we see that here in TableView. There's a brief delay before the table cell becomes selected. But if the user scrolls the table really fast, there's no selection at all.

But as you notice, once the user started moving their finger in the table view, and scrolling began, even though the selection was already visible, it immediately went away. Now the reason that happens is because the table view sends touchesCanceled to the-- to its subview after the-- after it detects that the user is trying to scroll if it's already delivered the touchesBegan. So you can actually get control over this with the second property, which is canCancelContentTouches.

Now there's a quick video here that shows what's happening in the default case. When the user-- when touchesBegan is delivered to this image view, it's going to get larger. And then as the user moves their finger around, when touchesCanceled gets delivered, it's going to get dropped and you'll see the ScrollView begin scrolling.

Now if we want to change this behavior, we can set canCancelContentTouches to No. And if that's the case, once the ScrollView has begun delivering the touches to its subview, it will not try and cancel that touch. However, if the user begins moving before the touch was actually delivered as touchesBegan, the ScrollView will begin scrolling. So we can see now if we've set this to No, we pick up this image view and we can begin dragging it around. But the ScrollView won't start scrolling and Touches Canceled isn't delivered until we lift the mouse then we drop it.

However if the user moves quickly, we begin scrolling. So just these two properties actually give you quite a bit of control over what's going to happen in the subviews. But you might want a little bit more control, especially depending on what subview of your ScrollView was actually hit. So there's two methods that you can implement in order to give slightly more control over this. Now these are actually methods on UIScrollView itself, not on the delegate. So if you want to implement these, you'll have to subclass UIScrollView and implement them in your ScrollView subclass directly.

So the first method is touchesShouldBeginWithEventInContentView. Now I already mentioned that ScrollView delays delivery of touchesBegan to its subviews, but what I didn't say is that immediately before sending touchesBegan to a subview, it first calls touchesShouldBeginWithEventInContentView on itself to give you a chance to override this and return No. And if you do return No, then touchesBegan and the rest of the touch sequence will not be delivered to the subview and instead it will be interpreted as an attempt to scroll the subview-- scroll the ScrollView rather.

Now on the other side is the second delegate-- or the second method, which is touchesShouldCancelInContentView Now as I mentioned if canCancelContentTouches is Yes, even if touchesBegan was already delivered to the subview, if the ScrollView notices that you've moved your finger far enough, it's going to cancel the touch on that subview and instead begin scrolling.

So you can, even if touchesShouldCancel-- sorry, even if canCancelContentTouches is Yes, before it sends touchesCancel to the subview, it will first call touchesShouldCancelInContentView on itself and if you return no, it will not send touchesCanceled, it'll continue delivering the entire touch stream to that subview and it will not begin scrolling.

Now if canCancelContentTouches is No, obviously this isn't going to get called because it won't try and cancel the touch anyway. So with those things in mind, I'd like to invite Eliza back up to give us another demo of some custom interactions using this-- these new methods.

Josh has explained how you can customize the delivery of touches to subviews of your UIScrollView. I want to show you what that looks like in practice by adding to my app a ScrollView where there are subviews that we might want to drag around to reorder.

So here's the same app from the previous demo. But I've added to it a drawer that slides up and down. And is itself a ScrollView that scrolls to the right and left that contains thumbnails of other images that you might want to view. So these images are already responsive to touches.

So you can actually tap one to switch which image you're looking at. If we want to make it so that you can drag them around in the ScrollView to reorder them, we're going to have to have a way to convince the ScrollView that touches that are moving in its bounds are intended for one of its subviews rather than as request to scroll.

So by default the ScrollView is going to try to scroll as soon as a touch moves in a direction that it can scroll. So here we have a right/left moving ScrollView. You can see that I can actually pick up one of these images as long as I move it only up. I can drag it around. But the moment that I start moving it in a direction that the ScrollView can scroll, my touch is canceled in the subview and the ScrollView begins to scroll.

So if I want to make it so that I can drag them to the right and left, I'm going to need to change the canCancelContentTouches property. So let me show you the code that's creating this some ScrollView. So to make it so that I can drag them to the right and left as well as up and down, I'm just going to add a single line setting canCancelContentTouches to No. And when I now build this, we'll see what the result is.

[ Period of silence ]

This time I can grab one of my images and I can move it back and forth and reorder the thumbnails in the drawer. So that's good. But there's still something a little sad about this which is that I can reorder among the thumbnails that are visible, but if I want to actually drag this guy that I'm, that I'm dragging around past the visible area, I'm pretty much out of luck. It'll be nice if the ScrollView would automatically start scrolling as I got close to the edge to make room to drag the thumbnail that I'm dragging further over.

If you've ever interacted with UITableVeiw and reordered rows, you'll notice that after you start reordering rows, if you get the row close to the top or the bottom of the table, it will automatically start scrolling so that you can reach the area above what you were seeing. So I'm going to show you now, how you can add auto scrolling to your application. And it's actually a lot easier than you might expect.

We don't need to subclass UIScrollView we're just going to add control for auto scrolling directly to our Root View Controller so back to the code here. I'm going to implement this method maybeAutoScrollForThumb. And what this is going to do, is it's going to check whether a particular thumbnail that's being dragged has been dragged close enough to an edge to be a signal that we should start auto scrolling.

And I'm going to call this application every time that the dragToThumbnail moves. So up here we have the delegate that these thumb image views-- that these thumbnail views are sending. When the thumb image view moves, we're going to say check whether we should start auto scrolling or maybe whether we should stop. All right, so let's see what happens here.

To find out if we should auto scroll we need to, we need to check whether we're close to the edge and we also need to keep track of which direction we would be auto scrolling if we're going to right, we-- if you get close to the left edge we'll auto scroll in the negative direction, and if we get close to the right edge we'll auto scroll in the positive direction. So that's the first thing that we're going to need to do in this method. Now if we are close enough to an edge to start auto scrolling, we're going to accomplish the auto scrolling itself by setting up a timer that's going to fire repeatedly.

Every time it fires we'll move the ScrollView by a very small amount. So that's going to be our second step, if in fact we do want to be auto scrolling. So to do this we're going to need 2 new instance variables. So I'm going to switch to the header file here for just a moment. And I'm going to add instance variables to keep track of the auto scroll direction that we want and the auto scroll timer that we're using to cause the auto scrolling to happen.

All right, back to the implementation file. So it's pretty easy to check if you're close, if your thumbnail is close to the right edge or the left edge. I've written some utility methods to do it so that this code will look simpler right now. So if it's near the right edge we're going to auto scroll in the positive direction. If it's near the left edge we're going to auto scroll in the negative direction. And if it's not near either edge, then we won't auto scroll at all.

And we'll set the direction to 0 to signal that. All right, so now supposed we in fact are near one of the edges. So we need to actually start the auto scrolling. In that case we're going to create a timer if we don't already have one. And I'm going to show you what that looks like. So this timer will fire repeatedly. Every time it fires it's going to call this autoScrollTimerFire method, which I'll write in just a second.

And we need to give it a time interval. So we want the scrolling to appear as smooth as possible. So I'm going choose as my time interval the refresh rate of the screen so that every time the screen refreshes the ScrollView will scroll by another few pixels. So 1/60th of a second is the maximum frame rate we can get. All right, let's write this autoScrollTimerFire method now.

The first thing to do when the timer fires is figure out how far you want to scroll. Now, there's a bunch of ways you can do this. For the purpose of this demo I'm keeping it really simple and I'm just going to scroll a constant 6 pixels in the appropriate directions.

So now in your own code you might actually want to be a little bit more clever about this. You could make it that the close the thumb is to the edge the faster you scroll. Or you could even set up something that it actually accelerates as it's been scrolling for longer. So I'll leave that to-- actually in the sample code I do the first of those 2 things. I make it so that it scrolls faster as you get closer to the edge.

So you can check that out if you're interested. Now, so, we're going to scroll by that distance. So actually to accomplish the scrolling all we need to do is to change the content offset property of our ScrollView. We need to change its X value by that distance. So I'm going to set, I'm going to grab the content offset, adjust it by the amount we've computed and then reset it. We're almost done. The one thing which we haven't yet done is arrange it so that the auto scrolling will ever stop. So we started it.

But as it stands, if I were to build this right now once it starts it would keep going forever. So to make it stop what we want to do is to have it stop in 2 conditions. So in the first case we want it to stop if the user has dragged the thumbnail close to the edge and then drags it back away towards the middle we want to stop the scrolling. We also want to stop if it the user lifts their finger from the screen and drops the thumbnail that we're dragging. So let me show you where that is.

The first of those cases we can handle in this maybeAutoScrollForThumb method. So we've told, we've decided what happens if we should be auto scrolling. If, in fact, we've decided that we shouldn't be, then we'll stop the timer from firing by invalidating it, and we'll nail it out as well.

To handle the case where the user lifts their finger, we're going to need to listen for a delegate method that the thumbImageView is going to send telling us that it's stopped tracking. So I haven't implemented that one yet. I'll do that right now. When the thumb image stops tracking we'll do the same thing. We'll invalidate the timer and nail it out. All right, I'm going to go ahead and build this and we'll see how it works. All right, bring up my thumbnail and I'll grab one of these images and pull it over.

So we're scrolling but the image is skittering all over the place. Not good, also it's letting us scroll way past the bounds what was visible. All right, we definitely don't want that. What's going wrong? Let me address the second problem first. So here where our auto scroll timer fires, let me get to that point in the code, we calculated a distance that we want to scroll and we changed the content offset.

We didn't put any check whatsoever to make sure that the new content offset was a legal one. We just blindly fed it to 6 pixels further over than it had been before. Now, when the user scrolls with their finger, you'll notice that the content will come to a halt at the end when it gets to the edge of what's visible-- or of what it-- of what the size of the image is, right.

It will stop scrolling and it might even rubber band if you have that setting on. When you change the content offset programmatically though there's no such check in place, so we're going to have to add one ourselves. We're going to have to check that this auto scroll distance is a legal value before we change the content offset by that amount.

So I've made another utility method that just adjusts the auto scroll distance to make sure that it's not too large or too small before we change the content offset. All right, so that should fix the scrolling past the bounds of what's visible problem. To fix the thumb image skittering all over as the auto scrolling occurs we would need to first understand why that's happening.

The problem is that the thumb image that we're dragging is a subview of our ScrollView. So like all the other subviews it gets scrolled off the screen as the content offset changes. Now that explains why it zooms off the screen as the auto scrolling happens. The reason it skitters back periodically, as you might have seen, is that we're also trying to drag that thumbnail. So every time I move the mouse slightly or rather move my finger slightly it would jump back to where the mouse was. And that had to have caused the appearance of skittering back and forth.

So to fix that what we're actually going to do is adjust the frame of the thumbnail that we're dragging to compensate for the scrolling. So every time the timer fires we change the content offset by 6 pixels. We'll also have the thumbnail move by 6 pixels in the same direction so that it appears to stand still. All right, to do that we just need a couple new things; first we need to have access to the thumbnail that we're dragging in this method. So the easiest way to do that is just to add it to the user info of the timer that I created.

So I'm going to pass the thumbnail along. And that way when the timer fires I can pull that thumbnail out of the user info and then I can tell it to change its content off, its own location by a particular offset which is equal to the change we're making to the content offset of the ScrollView. So these thumbs know how to move themselves by a particular offset.

So I've chosen to change its X coordinate by the same amount and leave its Y coordinate alone. So build that and see if it fixed the problem. Bring up the View, grab one of these thumbnails, start scrolling, and this time it stays nice and still as we scroll and as we get to the end of the visible content we stop.

All fixed thanks.

[ Applause ]

Well first let's talk about now that you've got your content in your ScrollView your users are panning around they're zooming in on it, you've got some custom interactions for some of your subviews. Now that you've been testing it, it works great on the iPhone simulator, you copy it down to the device and immediately it crashes because you've run out of memory. So how are we going to adjust, deal with that problem? Well, there's a few approaches that you could take.

But what we're going to talk to you about today is tiling your content, breaking it into smaller pieces and just displaying and loading the parts that you actually need to show the user what's visible in the ScrollView screen right now. So as we said, you might want to do this if you have more content than you can actually fit in memory. You might also want to do it just to improve load times. If you've got even large images that will already fit in memory, it takes a while to load them off the flash.

You may be able to reduce that time by tiling it or breaking it, breaking it into smaller pieces. You might also want to do this if you have to download content as needed. For instance the Google Maps application-- there's no way we could fit all of the map data on the phone.

So we pull pieces down and just tile it and just load only the parts that are needed to display what the user is seeing. So there's actually an example of this on the iPhone already in the form of UIScrollView, UITableView. And for those of you that have used this, I'm sure you're already familiar with cell reuse.

So once a cell scrolls off the top of the visible area of the TableView it gets pulled out and put into the reuse queue and then when the TableView asks you for a new cell you can pull it out of the reuse queue, fill out new content and it will scroll it back on the other side. So you can see that here, the ScrollView scrolls and we reuse those cells as they're scrolled off and they come back in on the other side, which is a lot less expensive than allocating new cells every time.

So that's great and it's in-- well it's only really in 2 dimensions. We might want something a little more complicated. So let's take a look at one of the photos that Eliza was working with. Now if we take that and break it into tiles we've got some squares that we can use as individual pieces that as they're scrolled around we can reuse them. So we'll overlay our ScrollView Frame on top of this again.

And now, as you can see, some of these tiles are currently fully visible, some of them are partly visible. And there's actually a row at the top that aren't in the ScrollView's frame at all. So those are the ones that we can use to reuse with new content as the user scrolls around. Now, obviously what you're going to want to do is just animate these tiles as these are flicks and they're going to fly all over the place.

It's going to be pretty awesome.

[ Laughter ]

All right, well maybe not, so, I actually just really like my demo and I wanted to play with my app. OK, so let's take a look at what is actually happening. User scrolls, some of those tiles go off, we fill out new content and move them back to the other side with the new content in them.

And the same thing works if we do it in 2 dimensions. The user scrolls diagonally, we pull some of these tiles out and fill them out with new content and put them in on the other side. So conceptionally it's actually very easy. And this is basically exactly what your TableView does except only in one dimension.

But where am I going to do this? So in this case you're actually going to have to start sub-classing UIScrollView. And you'll want to subclass ScrollView and override the layoutSubviews Method. Now, the one thing to keep in mind if you do this is that you absolutely want to call super. A UIScrollView does do work in its labSubviews method, and if you don't call super things like the scroll indicators may end up in the wrong spot.

So do subclass but yeah, call super. Once you've implemented your labSubviews, you need to figure out which tiles are currently visible so you can figure out which ones you can pull out and reuse in a new area of the ScrollView. Now, that's actually a lot easier than it might sound. You can just intersect the subview, the tiles frame with the ScrollView's bounds.

So the ScrollView's bounds define the area in the ScrollView that's currently visible and the frame of the tile obviously is its frame within its ScrollView's bounds. So any time that those things intersect, then that tile is currently visible. If they don't intersect, then it's outside; you can pull it out and reuse it with new content where you need it in another part of the ScrollView.

So that's the basic idea, let's get Eliza back up one more time to give us a demo of tiling content in UIScrollView.

I've written a subclass of UIScrollView that tiles its content just as Josh was describing. I'm going to show you some pieces of this subclass, particular the layoutSubviews method which is where the bulk of the tiling work gets done. But first, let me just tell you a little bit about the overall architecture of this subclass. And for that I'm going to look at the header file of my subclass.

So here we have the header file of my tiled ScrollView subclass. I want to bring to your attention three aspects of this class. First, we have this instance variable called a tile container view. What this is, is one big view that is actually a direct subview of the UIScrollView. And we're going to add the tiles of subviews of it, rather than adding them as direct subviews of the UIScrollView itself.

The reason for this is that if we want to enable zooming on our content at all the pinch; the pinching only works if you return a view from the delegate method viewForZoomingAnd ScrollView. So we can't return one of the tiles because then only that tile would be scaled as the user pinches. So we're going to embed all of our tiles in this container view in order to enable zooming. It also has the added advantage of allowing you to detect taps all in one place.

So I've made this a tap detecting view so that we'll be able to detect all the same taps that we could before. All right, the second thing I wanted to draw your attention to is something that Josh mentioned. We're going to use a data source model for this subclass. So the subclass is going to be responsible for laying out all of its tiles just as the TableView is responsible for laying out its cells. But it won't create the tiles itself.

It will ask its data source to create the tiles for it and it will also ask the data source to provide the content of the tiles. And it will ask the data source for the tiles using this one data source method titleForRowAndColumn. I'm going to ignore this resolution piece of this for now.

I'll talk more about that in a little while. So the last thing I wanted to point out is that we're going to use recycling as Josh mentioned. So we've got a set here of reusable tiles. Every time that a tile gets scrolled off of the visible area we're going to take it out of the view, stick it in our reusable tile set and then have it ready to provide the data source with it if the data, so that the data source can avoid repeatedly creating new tiles. All right, so let's switch to the implementation file and I'm going to show you how the layoutSubviews method is going to work.

The first step is to call super as Josh mentioned. The second step is to just grab the visible bounds of the ScrollView because all of our computations here are going to require knowing exactly which part of the ScrollView is visible. So now, we've got our visible bounds. We need to figure out which tiles are currently visible.

And for all the ones that aren't we're going to recycle them. So we'll do that by just iterating through all of the tiles that are subviews of our tile container view. So for each tile that's a subview of the tile container view we're going to check if it, if its frame intercepts the ScrollView's bounds. But, there's a bit of a hitch, which is that the tile container view that contains these tiles may have a transform applied to it.

If the user has zoomed it all then the coordinate system of the tile container view will not be the same as the coordinate system of the ScrollView. So we first need to convert the tiles frame into the coordinate system of the ScrollView before we check whether they intersect. So I'll do that here. We ask the tile container view to convert the tiles frame to our own coordinate system where we are the ScrollView.

Now, if the converted tile frame does not intersect our visible bounds we're going to recycle the tile by adding it to our reusable tiles set and then we're going to remove it from the superview. All right, so that's, so now, we've basically taken all of the tiles that we're there and we've pruned away all of the ones that are no longer visible so that we can reuse them in what comes up next.

So next, we are going to figure out what tiles we actually need to provide. We may have scrolled into a region that has some tiles that are missing so we may need to provide all of the missing tiles so that the image appears complete. So we have to figure out which ones are missing. So first I'm going to actually figure out which ones are needed. I'm going to figure out what rows and columns of our big tiled image are actually needed based on the visible bounds. So I'm going to paste a bunch of math here.

Don't be alarmed. The things-- the point to pay attention here are just these 4 lines. I've computed the first and last needed row and the first and last needed column based on the visible bounds of the ScrollView. So this is all going to be available by sample code. So you can look at it in more depth there.

[ Applause ]

So next, we're going to just iterate through all of the tiles that we figured out that we need and make sure that we've got them. So I'm going to go through all the needed rows and I'm going to go through all the needed columns. And for each row and column pair I'm going to check whether I already have a tile at that row and column. Now there's a lot of ways that you could do the bookkeeping here. You might, depending on your purposes, want to do this in a more complicated way.

For now, I'm doing it in an incredibly simple way. I'm going to switch back to the header file to show you how it's going to work. I've got 4 instance variables here which just keep track at all times of which rows and columns are visible. So I've got first and last visible row and first and last visible column.

So assuming that those instance variables contain the correct information, we can find out if we need a tile at this location by just checking whether the row and column that we're at is within the range that's specified by those visible rows and columns that we've been keeping track of.

So if the row is too low or too high then the tile is missing, and if the column is too low or too high then the tile is missing. All right so, if the tile is missing then we're going to need to create a tile and put it there.

And for that we'll ask our data source. So we'll call data source, ask for the tile of the row and column that we're at. And once we've got the tile we're going to set its frame so that it fits in the right place in our tile container view. So we'll figure out the frame we need which is based on the tile size and the row and column that we're filling. We'll set the frame.

And then, we'll add the tile to our tile container view. Pretty much, that's all you'll need to do. I'm going to add one more line here just for demonstration purposes. I'm going to draw all over the tile so that you can see the boarders when I run this, and that you can-- and so that you can see which tile is which. So I'm going to annotate the tile. You obviously would not want to do that in your own code if you were trying to present an image. So that's filled in all the needed tiles.

And the last step is just to update the values of our first and last visible rows and columns so that they contain the correct information for the next time through layouts and views. All right, so we're almost ready to run this. Let me just switch to the root view controller for a second and show you how you would write the method that actually, this is not-- there we go, write the method that actually returns the tile. So we're going to first ask the ScrollView to give us a recycled tile if possible.

So we'll DQ a reusable tile if there's one available. If there isn't then we're going to need to create one. I mean I initialized; it was a 0 frame because the ScrollView itself is actually doing the work of sizing the tiles. Then, I'm going to provide the tile with content.

Now, there's lots of different ways you could do this. You could be getting your content from online, you could be drawing your content directly. In my case I've actually cut up these images in advance and added them as resources to my project, so I'm just going to grab the appropriate piece. And I'm going to return it.

All right, so let's build this and see what happens. All right, so we've got our image and you can see that I've drawn boarders on the tiles. I've given each tile a unique number so we can track which one is which. As I scroll around you can see that we're using a bunch of different tiles.

There's a total of 9 of them actually. And the reason that there's 9 is that you might be seeing 9 tiles at once or pieces of 9 tiles at once. We're actually not going to create any other tiles besides these 9. We're just going to keep reusing the very same tiles over and over again.

There's number 9 again and so on. So now, I've added one more feature with this application which has to do with this resolution business that I kept ignoring in the code. So you might want, if you're writing a maps-like application or even just an application that shows a very large image. You might actually want to have the ability as the user zooms in the image to change the resolution of the image pieces that you're displaying.

So here as the user zooms out, what's going to happen is first the tiles will get scaled, so they're just-- the transform is being applied to the tile container view and the tiles are getting shrunk down. But, after I pass the half way mark they get replaced by tiles that are showing twice as much of the image or actually 4 times as much, twice as much in each direction, but at half the resolution. So I can now scroll around and you can see that there's fewer tiles making up the entire image. And I'm going to do it again because we also support a 25% resolution.

So as I get to the halfway point now I get my tiles replaced again. And this time they're at the minimum resolution and in fact the entire image is being made up with only 6 tiles. So this has one really good advantage, which is that loading an image to begin with when we're zooming all the way out on an image, it's really fast now.

I don't know if it was apparent on this computer (which is a pretty fast computer) when I was showing you the earlier versions of this application, but when I switched images there was a perceptible delay in the new image loading because it was actually opening an 8 megabyte file and loading it into memory.

Now, if I bring up my drawer and I switch images to say this one, it loads instantaneously, because it's actually only reading about 300K of data that comprises those 6 files that you see there. So now, if I zoom in on this, on this image what will happen is, I'll start replacing my low resolution tiles by higher resolution tiles. And now, I've gotten to the highest resolution possible and you can see that there's really quite a lot of them.

All right, so if you want more information about how this works, the resolution swapping in particular which I didn't show you the code for, take a look at the sample code which is available for download, and also feel free to come to the labs. There's one right after this session and then there's another one tomorrow. Both Josh and I will be there and we'd be happy to explain how you do this resolution swapping for tiling.

Thanks a lot.

[ Applause ]

Thanks again Eliza. So as I mentioned and as Eliza mentioned all this code is available as samples. And Eliza's put a lot of work into this. I think it's really going to be great. There's a lot of good information there. Please download it, make use of it, do some cool things with it. So now you've got all your content. You're scrolling it, zooming it, interacting with it, and not crashing when you put it on your iPhone. So everything's great.

There is this one last bit that I mentioned early on that I'd like to cover quickly, and that's nesting ScrollViews within ScrollViews. So I've got a quick video here that shows the places that-- some of the places we're using this in iPhone LS 3.0 already. So I'm going to kind of talk through it real quick. So you can actually now on your home screen, you know, swipe left to get to the Spotlight.

And within Spotlight there's a vertical ScrollView that you can scroll up and down. And let's page back to your home screen. Within Socks you can page the horizontal ScrollView at the bottom and then scroll vertically within that TableVeiw and then back again. Now both of these examples have a horizontal outer ScrollView with a vertical inner ScrollView. Obviously the inverse also works just as well.

UIKit actually takes care of all of the-- all of the work of figuring out which ScrollView is intended to be scrolled. It figures out whether the user's moving horizontally or vertically and picks the right one. You don't have to do any craziness with trying to forward touches between views or anything. All of that just happens. We, we fully support cross-axis nesting, vertical views and horizontal, horizontal and vertical-- that all works great. We use it all over the place ourselves and definitely encourage you guys to do it.

Same axis nesting, a vertical ScrollView and a vertical ScrollView, it will work. The interaction and the user experience is still subject to change. We haven't really settled on exactly what the experience is that we want in that case so we really discourage that kind of a nesting. It will work and there is a behavior that is defined, but that defined behavior may change. So yeah, please do the horizontal and vertical in each-- in one another-- that's pretty great. So looking back at what we've covered there are alternatives.

If you can make use of UITableView and UITextView or UIWebView you may not have to use a UIScrollView directly. But it's there if these things don't meet your needs. Also, custom interactions-- do these in subviews of your UIScrollView, not in a subclass of UIScrollView itself. We haven't really been clear on this over the past but we really-- the intention is that custom interactions-- touch detection, tap detection-- this is intended to happen in subviews of the ScrollView and the 2 properties and 2 methods that I mentioned-- 2 control touches are all designed to work with touches in subviews of the ScrollView. And both of those properties and both of those methods have existed since iPhone OS 2.0.

So they're not new in 3.0 despite the fact that when I was walking off I said these new properties. And finally saved memory-- load only what you need to display what the user is currently viewing. Try and improve your user experience that way. So if you have more questions after the session, Matt Drance is our Application Frameworks Evangelist. I'm sure you've all been seeing his name a lot this week. And you'll see his face in just a minute again. And UIScrollView Class Reference on developer.apple.com is also a great resource for information on UIScrollView. There are 2 related sessions. 1