App Frameworks • iOS • 54:51
Come learn about how to achieve the appearance of infinite scrolling in either one or two dimensions. We'll also look at how to change the resolution of drawn content during zooming, without requiring the use of CATiledLayer.
Speakers: Eliza Block, Josh Shaffer
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
All right, good morning, everybody. My name's Josh Shaffer, and I will be joined in a little while by Eliza, who's going to do some demos for us today. And today we're here to talk about advanced techniques for use with UI ScrollView. So if you've seen our UI ScrollView sessions in the previous couple of years, we've talked about a variety of different things, from tiling to zooming and all this kind of stuff. Today we're going to focus on four specific, interesting, advanced techniques for using UI ScrollView.
So the first technique will be infinite scrolling. This is the case where you have some content and you want to keep scrolling in one direction or the other direction forever and never hit the edge of the content. Maybe you've got some photos and you want to wrap through them and never get to the edge. So we'll talk about how you can do that using UI ScrollView. Second, we'll talk about stationary views. Now, there's a number of different reasons you might want to use stationary views.
The kind of things we're talking about here are views that remain pinned in place. So you can't just scroll in one orientation but scroll with the scrolling content in the other, sorry, not orientation but dimension or direction. So we'll get into that and look at a couple techniques for doing that.
Third, we're going to talk about customizing touch handling. We've talked about this in the past, so if you saw the ScrollView session two years ago or have looked at it on iTunes, you may have seen some talk about using multi-touch in subviews of UI ScrollView. But with the introduction of UI Gesture Recognizer in iOS 3.2 and 4.0, we've been able to do a lot of things. This has become much easier and much more powerful.
So we'll look at a couple different examples of things you can do to customize touch handling in your UI ScrollViews. And then finally, we'll talk about redrawing after zooming. And this is, as a general concept, is something we've talked about in the past. But we're going to look at one specific technique today that is really useful when you have small bits of content that you want to redraw at a higher resolution after you're done zooming. So we'll get through to all of these things over the next hour.
But just to get started, and make sure that we're all on the same page in case there's anyone here who hasn't done much with UI ScrollView before, I just want to do a really quick five-minute review of the basics of UI ScrollView and how you get it configured and get started using scrolling and zooming.
So right off the bat, if you have some content, there's only one thing that you have to do in order to begin scrolling it with a UI ScrollView. And that is to set your content offset. So this just tells the ScrollView how much content you have and how much it needs to be able to scroll in the X and Y dimensions.
So in this case, we've got our collection of photos, and we've just defined our content size, the width and height, to be the size of the scrollable content. And with that done, you can already allow your users to scroll up and down. So it's really, really easy to get started.
From there, you may sometimes, while you're using your application -- or rather, within your application while writing it, want to be able to find out programmatically which part of the content is currently visible. And that's also really easy to do. The way we refer to that in the -- in the Google Docs, is we refer to it as a "content size." And that's what we're trying to do in the world of UI ScrollView, is as the content offset.
So content offset is the point in your content that's currently visible at the top left of the ScrollView's frame. So in this case, we have no content to scroll horizontally. We can only scroll vertically. So content offset X is always zero. And our content offset Y, with the point we're scrolled to right now, is the point in that content visible at the top left of the ScrollView. So it's just that distance there on the left from the top of our content.
[Transcript missing]
So we can do that really easily, actually, and with a content size that isn't significantly larger than what's actually visible at any one time. So if we call that gray box there our content size, you can see that we've only made it maybe twice the size of what's visible on screen.
And once we've done that, then the idea will be that the user puts their finger down and scrolls the content to the side. We swap out the photos, so the one that went off screen we remove from our scroll view, and we bring a new one in. If that technique's not real familiar to you, there's a whole session from last year about how you can do paging through photos.
I would recommend looking at it on iTunes U and downloading the PhotoScroller sample. We won't get too much into the specifics of how you do that tiling bit today. But the interesting part is that now we're at the edge of our content again. So if we did nothing else, our user now would start bouncing against the edge, which is absolutely not what we want.
So we can fix this by doing just two small things. The first is we can adjust our content offset so that we get centered back in the scrollable area. So we'll do that, and that'll shift our content size back to be centered in that scroll view so that we can again scroll either left or right and not hit an edge.
But as you can see, if we just did that, our content would actually be back off screen again and not in the right spot. So once we've adjusted the content offset, we also have to then adjust the frames of our subviews by the exact same amount so that they end up back centered in that content area.
Now, I did this in two steps here just to illustrate that there's two steps that you have to take to do this. But if you do them one after the other in the same turn of your run loop, it will be completely transparent to your users. There will be no indication that you've done any of this. It will just happen all at the same time and, you know, completely flawlessly. So again, user puts their finger down, they scroll to the edge, we'll recenter our content offset and shift those subviews back so that they're centered on screen again.
So that's what we want to do. Where do we do this, and how can we implement it? Well, the basic idea that we're going for here is to relay out those subviews every time that the user scrolls. So a very natural place to implement this is in the layoutSubviews method.
LayoutSubviews is, as you know, a method on UIView that you can override. So in order to do that, we actually have to subclass UIScrollView. You could also do some of the same stuff by implementing the scrollViewDidScrollDelegate method. But we'll get to some other things later in the session that, for various reasons, it's interesting to have subclassed layoutSubviews. So we'll use layoutSubviews in this case.
Once we've done that, there's just two methods that we can call in order to do those adjustments we saw. First, we'll use setContentOffset to shift that content back, and we'll just shift it over so we're centered again. And then we'll use either setFrame or setCenter to adjust the offsets of those subviews so that they end up back where we want them in the center. So really not very much to it. It's a very simple concept, really. And to give us a demo of how that's all going to work and fit together, let's have Eliza come up and show it to us.
[Transcript missing]
I'm also going to set the scroll indicator style to white, because I think that's going to show up best against a dark background, so that you'll be able to see the scroll indicators jump to the center. All right, so now we need to go down here to our layout section and write the method that's going to do this recentering trick that Josh described. So I'm going to write a method called recenterIfNecessary, and in it, I'm going to compute whether it's time to recenter. And I'm going to do that by first finding out what our current content offset is.
And I also want to calculate what the content offset that would be perfectly centered in the middle of our content would be. So I'm going to find out what our content width is. And then I'm going to compute what would be that center offset x that would leave the exact same amount of content on both sides. And I use the bounds property of the scroll view to compute that.
The distance from the center is the absolute value of where we are now minus where we want to be. And if that distance gets big enough, so if we've scrolled far enough away from that center point, we're going to pick ourselves up and recenter ourselves. So there's a decision to be made about how much counts as far enough away from the center so that we should do this trick.
We don't want to do it at every frame, just because that would be an awful lot of work, so we want to allow for some scrolling to happen before we do it. Again, pretty arbitrarily, I'm choosing to allow you to get about a quarter of the content width away from the center before we'll pick you up and put you back in the middle.
You could experiment with different values. It shouldn't matter too much. So if we've gotten far enough away from the center, we are going to change our content offset by using the center offset that we calculated, but we're going to leave the y offset the same. There's my mouse.
Because we're only doing this trick now. We're only doing this trick in the horizontal direction, and we want to leave the vertical scrolling alone. You could do this exact same thing to get infinite scrolling in the vertical direction by adjusting your center point relative to the height of your scroll view as well.
All right, and the last step -- oh, sorry, one more step. So Josh mentioned that we need to change our content offset, and we also need to move any subviews of our ScrollView back over so that they don't appear to change position. We don't have any content yet, so I'm just going to write a comment that when we add some content, we need to remember to move it at this point.
And then we're going to call this "recenterIfNecessary" method from layout subviews. Layout subviews is called at every frame of zooming and at every frame of scrolling. So every time that the content that our ScrollViews bounds change, we're going to hit this, which will give us the opportunity to recenter. and I will be talking about how to change the resolution of the image.
All right, so you just see an empty blue view. But as I start scrolling, I hope that the content, that the scroll indicators are visible. I hope they are. So if you look at that bottom one, as I scroll, if I get close enough to the edge, it jumps to the center.
Can everyone see that? Yeah? All right, cool. So it jumps to the center because it hit that far enough away from the center thing that we added, and I can do it in the other direction, too. It will jump to the center. And so, of course, the scroll indicator gives away the trick, so we don't actually want that to be there, but this is just to illustrate what's going on with the technique. So next, let's add some content to this.
I was thinking when I was preparing this demo about what kind of content you would want to scroll infinitely, and I was also trying to figure out what you could do if you have no artistic skills in order to add that. So I made this little -- here, I'll add this drawer.
So I made this little subclass of UIView called a BuildingView, and what it does is it produces a random building, which is a different color and with different style windows and stuff. And so I figured we could make this -- we can scroll infinitely down a street where we'll keep putting more and more buildings there to fill the space as you scroll.
So we're going to use that here to add some content that we're going to tile as we do this infinite scrolling. So I'm going to import that building header. I'm also going to add a class continuation to infinite scroll view subclass so that we can add some -- Ivar's that we're going to need.
So the idea is going to be that we're going to tile these buildings. And if you've seen our previous sessions, you'll see that we've almost always, it almost always comes down to tiling. So we're going to keep an array of buildings that are visible right now. So we're going to need this.
We're going to add a new building to a container view that contains all the buildings. The reason for doing that rather than adding them directly as subviews of the ScrollView is that later when we add zooming, it will be convenient to have a super view that we can return from the ViewForZoomingInScrollView method. So let's go ahead and create these here. Scroll down. So we're going to create this array of visible buildings, and we're going to create our building container view.
and we'll add it as a subview of the ScrollView. I'm using the content size to determine the size of the building container view, so it will just be the same size as the ScrollView's content, although it won't in fact be full of buildings. We're going to tile them so that only the visible ones appear at a time.
All right, so now in layout subviews, let's create these buildings. So the idea is going to be-- oh, sorry, I forgot one other thing that I wanted to do. I want to change this content height. So instead of 500, which I picked arbitrarily, we'll make it the height of the tallest possible building, which my little building view subclass can tell us.
And we also want to get rid of these scroll indicators while we're at it. We don't want to see that horizontal scroll indicator, because it indicates that we're jumping back to the center, which we're trying to hide. So I'm going to say don't show a horizontal scroll indicator at all.
Okay, so in LayoutSubviews, we need to calculate what are the visible bounds of our ScrollView right now, so that we can fill that visible width with buildings. So we get the bounds of the ScrollView, and we can then use those to compute the minimum and maximum visible X values of the ScrollView.
And at this point, we can now fill that visible region with buildings. And I'm going to take a bit of a shortcut here, because we've gone through tiling in detail in a couple other sessions, and it's all in the sample code. So I'm going to make a little helper method that does the tiling for us. You can check out how this is implemented in the sample code. I'm going to call it from layoutSubviews.
What it does is it takes the minimum x and the maximum x, and it makes sure that there are buildings in that entire area. It also makes sure that the buildings that are outside of that area have been removed, so that we're not having an incremental growth in memory usage.
I'm going to paste the actual work there that gets done. Don't worry too much about this. You can see it's not actually that much code. All right. So once we've done that, and we've got our buildings tiling the area, all we need to do is remember to come back here and move the content by the amount that we adjusted the content offset so that it appears to stay still.
So we'll do that by iterating through the buildings in our visible buildings array. We'll find each building center. We'll adjust it by the amount that our content offset just changed. And then we'll reset it as the building center. And we should be good to go. So I'm going to go ahead and run this.
So here's some buildings. And as I scroll, new ones come in. I can scroll back the other way, and we see new ones appear that way. And I can just keep doing this for as long as I want, and we never get to the edge. So indefinitely many infinite buildings. All right, so that's-- So that's an infinite scroll view, and I'm gonna turn it back over to Josh.
All right, thanks Eliza. So, infinite scroll views. Next up, we have stationary, header, footer, and other random views that you might like to keep in place. So, what are we trying to do here? Well, let's say that we've got some large piece of content that we wanna zoom in on, but we wanna have a piece of a header view up at the top that remains stationary, it will not zoom with the content.
So, when the user zooms in on that large image there, the header will not zoom in. As the user scrolls that image horizontally, that header will remain pinned in place. But as the user scrolls vertically, we want this header to scroll off to make sure that we have the maximum available area to actually view that photo.
So we really want that header view to be part of the scroll view, because we want it to scroll vertically. We just want to prevent it from zooming or scrolling horizontally. And then when the user zooms back down, we just want it to slide back in with that content. So this is pretty easy to do, too.
Let's take a look at where we might configure our views in order to get this kind of behavior. Because if you've used zooming in UIScrollView before, you know that normally you'll just have one subview of the UIScrollView, and you always use that as the view that's zooming. So it's a little bit unusual to actually have a second view in your zooming scroll view. But it does actually work, and you can do it. We just have a couple things that we have to do to make sure it works the way we want.
So we'll have this large external UIScrollView that's going to take up our full screen. And then we're going to add two subviews to it. The first will be that header view. And again, this will not be the view that's zooming, and it will not be a subview of the view that's zooming. It's just going to be a direct subview of the scroll view. We'll then add another UIImageView subview of this scroll view, which we actually will return from the ViewForZoomingInScrollViewDelegate method. So we now have two direct subviews of that UIScrollView.
So let's see what would happen if we just did that and did nothing else and allowed the user to try and zoom in. They would put their fingers down, they'd pinch them apart, and well, it almost did what we want, but there's still a couple things that are wrong.
First of all, that header view actually shifted off screen, which is exactly what we didn't want to have happen. And it also, you can't really tell from this, but believe me when I tell you that our content size is now incorrect, and we would not be able to scroll all the way to the bottom of our image anymore.
But at least we are partway there in that the header view did not zoom, so we've got that going. So first let's fix the header positioning. As we said earlier, the position of the scroll view, or the part of the scroll view that's currently visible is the content offset.
And in this case, we only care about the content offset.x because we wanna keep that header view stationary horizontally. So in our layoutSubviews method, now that we already have our scroll view subclassed, we can just override that layoutSubviews and make sure that our subviews frames origin x always matches the content offset.x.
Because as we scroll, the content offset.x is always the portion of the content visible at the left of the scroll view. So if we make our subviews frame always match that, it will just appear to remain stuck in place, a pin to that left side of the scroll view.
But now I did mention that there's this one other problem, and that's our content size. So I've highlighted that gray box, which indicates our content size, so it's a little easier to see. And the reason this happens is that ScrollView zooming, when you return a view for zooming in ScrollView, the ScrollView automatically, while zooming, sets its own content size to match the frame size of that zoom view.
So while you're pinching in, or while you programmatically zoom in, at every new content size, or every new zoom scale, rather, the ScrollView has adjusted its own content size to match the new zoomed size of your zoom view. But in this case, we have a bit of extra content that's not included in that zoom view. We've got that header on top, and the ScrollView doesn't know about that. So we actually need to include that extra height ourselves. And we can do that because UIScrollView adjusts its own content size by calling selfSetContentSize.
So in our subclass, we can override setContentSize and add in that extra header height that the ScrollView didn't know about. So we can just add that additional height from the top to calculate the appropriate larger content size, and then call superSetContentSize to actually apply that. And once we've done that, that will increase the available content size to just grab the extra space we were missing from that header. Now, Eliza's going to come back up and do another demo for us, but she's actually going to just show something slightly different that you can do with stationary zoom. So we're going to have Eliza come back up and show us how to do that.
Hi. So I decided to add a stationary view to this. We don't really have anything that a header would be useful for in this little demo app. So I'm going to add a moon to the app. And the idea is going to be that the moon will scroll with the scroll view vertically, so you can scroll it offscreen if you scroll vertically. But the moon will stay fixed in position horizontally so that as you scroll the buildings by, the moon stays put horizontally. So we'll use that as an example of a stationary view. There we go. Okay.
So we'll need to add, we'll need to declare this moon view. We'll make it an image view. And I've drawn a little picture of a moon that I've loaded in as an image. We can create the moon here after creating our building container view just by allocating a new image view with that image.
And I'm going to insert the moon view as a subview of the scroll view directly as a subview of the scroll view itself rather than the image view. And I'm going to insert the moon view as a subview of the scroll view itself rather than as a subview of the building container view because we want the moon to not zoom when the buildings do zoom. So I'm going to insert it below the building container view because it seems like the buildings should pass in front of the moon if they happen to overlap it. So all right.
Now we've got this moon. We just need to determine what its position should be. And I've chosen some good values for its center as insets relative to the top and right, right edge. So I just picked these values by trial and error because they looked good. And in layout, when we layout subviews, what we'll do is after doing the tiling, we'll make sure to position the moon. And that way we can make its X position dependent on what piece of the scroll view is currently visible.
So the center of the -- the moon's X center will be relative to the maximum visible X of the scroll view. And we're going to put that in the center of the moon. And then we're going to put that in the center of the moon. And we'll just subtract the right inset off of that so that it's slightly on screen.
And the effect of that will be that as we scroll, its position will change because the maximum visible X will change, and which will sort of paradoxically have the effect that it will appear not to be changing because as you're scrolling, it will actually stay fixed on the right side.
And then the Y center is going to be an absolute value that's just relative to the top of the scroll view, and that will have the effect that the moon does scroll in the vertical direction. So we'll just set the moon center, and go ahead and run that. There it is. If I scroll vertically, it scrolls right off. But as I scroll horizontally, it stays fixed and the buildings pass right in front of it as we wanted.
All right, thanks again, Eliza. So just to keep you all on your toes, we've got two more back and forths between slides and demos. So don't get too bored yet. The next thing we want to talk about is customizing touch event handling. So what we're talking about here is adding custom multi-touch handlers to subviews of your UI ScrollView.
So if you've seen the UI ScrollView sessions before, as I said earlier, we talked about how you could do this using regular multi-touch-- touches began, moved, ended, and canceled. There's a variety of methods on UI ScrollView that let you control how those touches get delivered to subviews of the ScrollView.
But it's all quite actually fairly complicated. With the introduction of UI Gesture Recognizer and iOS 3.2, this has become much, much easier, and in fact, much more powerful. So there's a lot of ways that you can very easily add custom interactions to UI ScrollView using gesture recognizers. So let's look at one of them.
Let's say that we've got this large amount of content, and we can scroll around in it. But we want to add the ability to swipe in from the left or the right, or swipe up from the bottom or down to the bottom. You've seen this yesterday in the keynote with the new mail interface in Portrait. You can swipe in anywhere in the message content area.
And even though that area is scrollable, when you do a fast swipe, you actually bring in the mailbox list. So we can do the same thing using UI ScrollView. And it's actually been made even easier to do more complicated interactions in iOS 5, which we'll see in just a second.
So the idea here normally is when a touch comes down in a scroll view, as the user moves that finger around, the default behavior is going to be to scroll the content. But we really want to add the ability to swipe in from the bottom without moving the content and animate some other view into place. We could then swipe down in that same area and animate that view back out. But the actual scroll view hasn't scrolled at all. We haven't moved our scrolling content in any way.
So how would we do that? Well, it's, as I said, now actually possible in iOS 5 to even more greatly customize the interactions of your gesture recognizers with the ScrollView's gesture recognizers. Because UI ScrollView actually uses UIPanGestureRecognizer and UIPinchGestureRecognizer to implement its own panning and zooming. And you can now add interactions that are dependent on those gesture recognizers. So the new properties in iOS 5 are PanGestureRecognizer, which gives you back the ScrollView's UIPanGestureRecognizer, the one it's actually using to track the touches for scrolling. And PinchGestureRecognizer, which gives you the UIPinchGestureRecognizer that it's going to be using for zooming.
So with those properties, we can now set up more complicated interactions between our own UI gesture recognizers and the scroll views, which gives us a lot more power to customize the way our views will interact with the scroll view. So for the example we were just looking at, let's take a look at how we can make use of that.
So first, we'll assume that we're in a place in our code where we're actually configuring our UI view. So a common place where you would do this kind of logic is in your view controller's viewDidLoad or in your load view, where you're creating and configuring your views. So let's say we have access to our UI scroll view in this place. I've got a method here called selfScrollView, which returns the scroll view we want to customize.
Then we just want to create our new gesture recognizer. So we'll allocate and init a new UI swipe gesture recognizer using initWithTargetAction. And we're going to try and create that swipe up recognizer that allows us to bring in an extra view from the bottom of the screen. So as you know, with UI swipe gesture recognizer, we just have to set its direct view and set its direction. So we'll pick UISwipeGestureRecognizer direction up, which gives us that swipe up. And then we just add it to our UIScrollView.
Now, this would almost be everything we have to do, but if you tried it, you'd actually find out that it didn't work. And the reason for that is because UIPanGestureRecognizer actually requires less movement to recognize a pan than UISwipeGestureRecognizer requires to recognize a swipe. So you have to move your finger farther to recognize a swipe than to recognize a pan. But the problem is we have both a swipe and a pan on this scroll view, so the pan will always recognize first and will always win.
So if we just put this swipe gesture recognizer on here, it would actually have no effect. But with one extra line of code, we can fix that, thanks to these new properties in iOS 5 that give us access to the scroll view's gesture recognizers. So we can call scrollView.PanGestureRecognizer, which will give us back that pan gesture recognizer the scroll view will be using. And we can just tell it that it should require our swipe gesture recognizer to fail before it's allowed to succeed.
And with that one line of code, even though our pan would normally be trying to recognize earlier, it's going to wait until after it's sure that the user isn't swiping before it begins to pan, which will cause your scroll view to not pan until it's sure that there's no swipe.
So that's the exact behavior that we're looking for in this little sample we were looking at. Now, of course, there's one small problem with this, which is that we've now created this large area over the entire scroll view, which recognizes swipes up. Of course, the problem with that is that now we've actually interfered with scrolling behavior in a large part of this scroll view. We probably don't want to require this delay every time the user's moving their finger up before we allow scrolling to begin, especially not if it's really far away from the edge we're trying to swipe in from.
So we really want to just target the area that the swipe is active in to that bottom area of the scroll view down there. And we can do that with just one small modification to what we already created. And we'll just use the gesture recognizer should receive touch delegate method to limit the area that the swipe gesture recognizer will receive touches from. So we'll get our scroll view again because we need to figure out which part of the scroll view is visible so we know which area to allow touches from.
We'll get the scroll view's bounds, which is the visible part of the scroll view, and then we'll get the touch's location in this area, which is the visible part of the scroll view. So we'll just do a little bit of modification in the scroll view so that we can compare it to the visible bounds and just make sure we limit it to the bottom part of the visible bounds. Then it's just a really simple if statement to check to see if our touch point Y is within that bottom portion.
In this case, I've said 75 points from the bottom of the screen, but, you know, you might pick something different depending on your situation. And if it is in that -- if it's not in that bottom area, we're going to return no to say that our swipe gesture recognizer should not receive touches in that area. It should be in the upper, you know, two-thirds, three-quarters of the screen.
Otherwise, we'll return yes, which will allow our swipe to be recognized when touches begin in the bottom portion of the screen. So again, just to continue to emphasize that there's a variety of different ways that you can use these techniques to do different things, Eliza's going to come back and show us how we can do something else using gesture recognizers in UI scroll views.
All right, so I'm going to add a gesture recognizer that's going to allow us to press down on the moon, pick it up, and move it to another location. So this is going to be-- it actually is going to be surprisingly easy with UI gesture recognizer. If any of you were here two years ago and saw our ScrollView session then, you may recall that we did a bunch of touch handling with dragging views around in a ScrollView.
But we had to do an enormous amount of work to make it so that the ScrollView would interpret those touches as intended to move one of the subviews, rather than as intended to pan the ScrollView. With gesture recognizers, it becomes incredibly easy. We're just going to create a long press gesture recognizer, which will recognize when you hold your finger still.
And because holding your finger still on the ScrollView doesn't even compete with panning, there isn't even going to be any need to set up an interaction explicitly between the ScrollView's pan gesture recognizer and our press recognizer. It will just happen automatically. The correct touches will go to the long press gesture recognizer.
It will recognize, and then it can be used to track movement of the view that we've picked up. So here in my initWithCoder method, I'm going to create a long press gesture recognizer. Whoa, that wrapped in a surprising way. Let's just get that visible. And we're going to use ourself as the target, and we'll write this. write this handleMoonPress method in just a minute.
I'm going to add that gesture recognizer to the moon view. There's two more things I have to do which are slightly irritating to get this to work. One of them is it's all well and good to add a gesture recognizer to this moon view, but if the moon view isn't accepting touches, then the gesture recognizer will never see any touches.
And in this case, since the moon view is a UI image view, which is the only subclass of UI view that by default has user interaction disabled, we need to make sure to turn user interaction on for the moon view or else we're not going to see any of these presses. So we set user interaction enabled on the moon view.
The other problem is that the moon view is behind our building container view, and the building container view takes up the entire content of the scroll view. And it does have user interaction enabled by default because it's a regular view. So we're going to have to make sure that we're not going to see any of these presses. So we set user interaction enabled on the moon view. The other problem is that the moon view is behind our building container view, and the building container view takes up the entire content of the scroll view. UI view.
It's going to block all the touches from getting to our moon view. We can fix that in a bunch of different ways. The simplest one is just to turn user interaction off on the building container view. So we'll do that so that the moon can get touches. All right.
So now that we've done those things, we can go ahead and implement this handle moon press method. And the idea here is going to be we're going to find out what state the recognizer is in, and we're going to do different things depending on which state it's in. So this is going to be a giant switch statement.
Oops, sorry. Get that all on screen. Okay, so we care about three or four different states that the gesture recognizer might be in. We care when it begins. That's after you've pressed for long enough that the gesture you're doing now counts as a long press. We care if it changes. It changes when, after recognizing you move your finger, and at that point we're going to want to change the location of the moon view.
And we care when the gesture ends or is canceled, because we want to, at that point, drop the moon view and leave it in its new position. So, as a springboard engineer, I decided to make the moon view look like it gets picked up in the same way that we make an icon look like it gets picked up.
And that way is, we do three things. We pull the icon to the front so that as it moves it passes over all of the other views that are on the screen. And we change the transform on an icon. We make it slightly bigger so that it kind of looks like it came towards the moon. And then we also change its transparency to make it slightly transparent so it has this kind of grabbed appearance.
So we'll do the same exact thing to the moon. Pull it to the front of our sub view, so it's now going to pass in front of the buildings. And with a little animation, we'll set its transform to be slightly larger, a scale transform that just makes it grow a little, and we'll reduce its alpha slightly. So now the moon will look grabbed. When the gesture changes because you've moved your finger, we're going to just reposition the moon. So we can do that by setting its center to the new location of your touch in the scroll view.
And finally, when the gesture ends, we'll undo the things that we did when it began with an animation. With the same duration animation, we'll set its transform back to identity, set its alpha back to 1, and when that finishes, we'll send the moon view back to the back of our subviews so that buildings once again pass in front of it. Alright, so with that in place, we can go ahead and try this out.
All right, so here's the moon. I'm going to grab it. It zooms up like we wanted. And then I start moving it. And I don't know if you noticed, but there's a bit of a glitch here. As I grab it and as soon as I start moving, it jumps up.
So we're going to want to fix that. There's another really big problem, which is that if I pull the moon over here and let go, it jumps right back to where it started. So let's go back to the code, and I'll try to explain why both of those things went wrong.
So the first problem is that jump. And the culprit here is that when the gesture changes, we adjust the moon's center point to be the new location in the ScrollView. But you might not have grabbed the moon from its center point. You might have grabbed it from the edge.
And so when we start, we immediately set its center to be the location of your finger, which causes the moon to jump from the point of it being under your finger to the center of it being under your finger, which has that kind of unpleasant effect. So there's a really easy way to fix that. So the first thing we want to do is to add an IVAR, which is to remember when the touch comes down, we want to remember how far away the touch was from the center of the moon.
To remember the touch offset from center, and then when the touch starts in our gesture recognizer began, we can just record what that offset is. It's the center of the moon minus the location in view for each of the X and Y components. And then finally, when we set the moon center, we don't want to just blindly set it to the current location. We want to set it to the location offset by that amount.
All right, so that will fix that jumping problem. The other problem was that when I dropped the moon, it jumped right back to the upper right corner where we had initially placed it. And the reason that that's happening is that in layout subviews, we're telling the moon to be located there. And it turns out that layout subviews gets called for a whole lot of reasons.
And so we're hitting our layout subviews, and that's as soon as I drop the moon, and it's pulling it back up to the upper right corner. So we can fix that by making sure that in layout subviews, instead of setting the moon to a fixed position, we remember its current position and we set it to that.
So instead of using these pound defines to place the moon, I'm going to add two new IVARs, a top inset and a right inset. In init, I'll set their values to be the same values we were using before. So the moon will start off in the upper right corner.
The end, the touch end part of our gesture recognizer code will adjust and Ivar, will make the right inset dependent on the current bounds of the scroll view, so that we still keep it relative to the current visible area of the scroll view. And finally, in LayoutSubviews, we can use those new variables instead of using the pound defines. So I'll go ahead and run. That and these problems ought to be fixed now.
So no more jumping. I grab it and it nicely starts moving smoothly. And if I drop it over there, it stays there and I can continue scrolling. So that's gesture recognizers with ScrollViews. All right, thanks one more time, Eliza. Okay, our last section now is adding zooming into our UI ScrollView.
And the specific thing that we want to talk about now is once you've zoomed in, and I'm sure you've seen this if you've used zooming in your ScrollViews yourself, your content starts to get blurry, and you really want to redraw it at a higher resolution so it looks really crisp and nice at the higher zoom scale.
So, you know, that looks basically like the user puts their finger down, zooms in on your ScrollView, and you can see that that content that we just zoomed in and made really nice is actually pixelated and blurry now, or hopefully you can see on there. What we'd really like to do is redraw that and be much more crisp and high resolution at this higher zoom scale.
So as I mentioned earlier, we're not going to talk specifically about how to do that on the main part of your content today. We have two previous sessions that you can see on iTunes, where we've talked about using tiling, using CATiledLayer, and just using your own custom tiling with UIViews that are from previous sessions of WWDC in previous years.
And there's actually sample code from both of those sessions that's still available on developer.apple.com. One of them is the ScrollView Suite, and the other one is the PhotoScroller sample. So if you want to see how to do this for your main content area, I would go check those things out. They're great resources for that. What we want to talk about today is smaller pieces of content that are maybe incidental to your main image.
In this case, we have this extra label at the bottom, and we have the exact same problem with the label. When we zoom in on that, you can see that our text has gotten very pixelated, and it's blurry. It just doesn't look very nice. We'd really like to redraw it and have it be really crisp and higher resolution once we've zoomed in.
So there's a really nice little trick that you can use to redraw this content, as long as you're keeping it to fairly small content. I would strongly encourage you to not use this for anything that's very much larger than a part of the screen when you're zoomed all the way in.
If you're doing something that's maybe 20 times the size of the screen, not only is it a bad idea, but your app will probably run out of memory and crash. So keep it to small pieces of content. But in that case where you can use it for small pieces of content, it's really convenient.
So the place where we would want to do this is in ScrollViewDidEndZooming with View at Scale, because we don't want to try and redraw our content while we're zooming. Redrawing content is a very expensive operation, so we only want to do it when the user is actually done zooming and has lifted their fingers, and we know the new scale we're at. So ScrollViewDidEndZooming with View at Scale will tell us exactly when that happens, and conveniently also tells us the scale that we've stopped zooming at, so we know what scale to redraw our content at.
So we can actually fix this problem with just one line of code, again provided we have a fairly small amount of content we're trying to redraw. And that line of code would just be the setContentScaleFactor method, passing in the scale that we told that we were at. Now, the content scale factor of a view is basically just a multiplier applied to the bound size of your view, used to determine how big the backing store should be for actually backing your draw rect on that view.
So if you have a 100x100 view and a content scale factor of 1, your backing store is 100x100 pixels. If you had that same 100x100 view with a content scale factor of 2, the backing store that you're actually drawing and filling with pixels is 200x200 pixels. So you just always multiply the bound size times the content scale factor, which will tell you how many pixels will be allocated to fill your content with.
This is actually exactly how retina display devices create content. So you can create higher resolution backing stores without you having to change any of your drawing codes, so that you can deploy the same application to both iPhone 3GS and iPhone 4 without having to write separate code in your draw rect. And on iPhone 4, everything just looks crisper. That's because all your views get a content scale factor of 2 set on them by default.
Now, of course, because they have this extra scale factor by default, we actually need to take that into account when setting our content scale factor, in this case as well. Because if we didn't, we would lose that initial 2x scale on high resolution retina display devices. So we want to actually do one last thing here, which is also multiply in that screen scale factor. So we just get our UI ScrollViews window, get that window screen, and get that screen scale, and then multiply that in before we set our content scale factor.
This makes sure that on those high resolution retina display devices, we're rendering at the correct resolution and don't still end up with some pixelation, even when we thought we were fixing the problem. So that's really all there is to it. And to give us a last demo of how we can use that in our Buildings app, Eliza will come back.
Hi one last time. Switch over. Okay, so let's add zooming to this. It's going to be a bit of a trick to get the zooming working with the tiling, so I'm just going to show you how that works. So the first thing we need to do is implement the view for zooming in ScrollView delegate method. So we need our ScrollView to have a delegate. Because all of my logic here is in this infinite ScrollView subclass, I'm just going to make it be its own delegate. So in the header, oops.
I'm going to indicate that this class implements the UI ScrollView delegate protocol. And then switching back to the implementation file in init, I'm going to set myself as my delegate, and I'm going to set the minimum and maximum zoom scale properties to some values that will allow for zooming. I'm actually picking a pretty large maximum zoom scale so that we'll really see the pixelation of the buildings as we zoom in, and then we can see it crisp up. Eight times is maybe a little more than you'd really need.
All right, so now that we've done that, we just need to implement the UI ScrollView delegate protocol. And in particular, we need the single method view for zooming in ScrollView, and we're going to return our building container view to cause that view to be the one that gets the transform applied to it. So we can run this as is.
[Transcript missing]
All right, so we've got some buildings. Let me find a nice tall one to zoom in on. So here we'll take this purple one with circle-- oops-- and we'll zoom in. And as you can see, as I zoom in, it gets really pixelated. And when I drop it, it stays really pixelated. I hope it's visible that that got incredibly blurry as I finished zooming.
So we can use the technique that Josh just described to really easily change the content scale factor of these buildings when the zooming ends. And that will cause them to be redrawn with a much larger backing store allocated. And the reason that this works is that most drawing that you would be doing in your draw rect is vector-based. And so when a larger backing store is allocated for it, that very same drawing code that I don't have to adjust at all will simply draw crisp, larger shapes. And this works for text as well, as Josh mentioned.
So I'm going to implement another ScrollViewDelegate method. And I'm going to use the same method to take advantage of this. ScrollView did end zooming with view at scale. And in that method, I'll calculate the scale factor that we want, which is my Windows screen scale times the new scale that we just landed at.
And I'm just going to iterate through all my buildings in the visible buildings array and just set each building's content scale factor to be that new value. And you might think that a shortcut here would be to set the content scale factor on the building container view, since that contains all the buildings.
But that would not work, because we need the drawing code to be rerun at the higher content scale factor. And the building views themselves are what's doing the drawing. Setting the content scale factor on the container would have no effect at all. So there's one-- well, let's see how that works.
I'll show you one further thing we need to do. So as I zoom in on this building, it gets really pixelated. But then when I let go, it crisps up with no further work than just that one content scale factor trick. So the one problem here is that as I finish, oops. So if I zoom in, we get this nice crisping up effect.
But then as I continue to scroll and new buildings come in, the new buildings are really pixelated. And that's because we're not adjusting the content scale factor of buildings that weren't already on screen when we finished the zooming. So we need to add one further thing, which is when we add a new building, which is down here in my secret tiling code, I have this method that's the funnel point for adding a building.
So we also want to add those same lines of code to that method so that every time a building is created, it's created with the appropriate content scale factor to cause it to draw at the right resolution. And so that will allow us to really scroll up and have nicely zoomed in buildings. So I'll start by zooming in. Nice, crisp drawing. And then as I continue to scroll, the new buildings that come in are crisp as well. So that's the redrawing after zooming.
All right, so the one thing that Eliza left out of that is that those buildings are actually pretty large, right? Some of them took up almost the entire screen, and she zoomed them in eight times. So if you had a view that was taking up most of the screen and you zoomed it in almost eight times, some of those buildings were taking up upwards of eight megabytes of RAM just to draw that backing story for that. So, probably don't want to do it with really big buildings. You would only be able to have a few before you'd all of a sudden be out of memory and couldn't do anything else.
So it's really good for small stuff like text labels, not great for really big things like buildings. So there's more information available. Bill Dudney is our Application Frameworks Evangelist. Of course, the ScrollView Programming Guide for iOS is available online, and the dev forums are a great resource. There's a TableView Changes, Tips, and Tricks session later this week. If I could read that. read that. Knob Hill, Thursday at 2 o'clock. Thanks.