Video hosted by Apple at devstreaming-cdn.apple.com

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: wwdc2012-223
$eventId
ID of event: wwdc2012
$eventContentId
ID of session without event part: 223
$eventShortId
Shortened ID of event: wwdc12
$year
Year of session: 2012
$extension
Extension of original filename: mov
$filenameAlmostEvery
Filename from "(Almost) Every..." gist: [2012] [Session 223] Enhancing U...

WWDC12 • Session 223

Enhancing User Experience with Scroll Views

Essentials • iOS • 53:50

Scroll views can be used in many different ways to create familiar and immersive user experiences. Come see what's new with scroll views, learn how to present scrolling content in a page view controller, and even how to enhance your OpenGL games with scroll views.

Speakers: Eliza Block, Josh Shaffer

Unlisted on Apple Developer site

Downloads from Apple

HD Video (503.7 MB)

Transcript

This transcript was generated using Whisper, it may have transcription errors.

Good afternoon, everybody. And thanks for coming back to another session about UI ScrollView. For those of you that have come in previous years, welcome back. And for those of you that are new this year, thanks for coming. We've got some very cool things to talk about today again, I hope. And if you've seen the previous sessions, either in 2009, 2010, 2011, You're probably familiar with how we're going to begin the presentation today, but that's just going to be a couple minutes, and then we'll get into a lot of more interesting things. So first off, we're going to start about just a little bit of review about how to configure your UI scroll view and how UI scroll views work, just the basics. So we're on the same page.

Once we finish that, we're actually going to look back at something that we talked about at WWDC 2010, which is building a great photo browser that feels just like the photo browser that you find in the Photos app on iOS. And the reason we want to review this a little bit more today is because with the introduction of a scrolling mode on UI Page View Controller in iOS 6, building this photo browser has become significantly easier than it was when we last talked about it. After we get done with that little bit of review, then we're going to talk about something brand new that we've really never even talked about at all before at WWDC. And that's integrating UI ScrollView into your OpenGL games and applications. So if you're sitting there thinking, well, you know, I don't have an OpenGL game or an app, so maybe I'm just going to get out when that comes up. But don't worry, there's actually a lot of really important things that we're going to discuss in the context of the OpenGL that are actually broadly applicable to all UI scroll views. So if you use UI scroll view, UI table view, UI text view, UI web view, or even the new UI collection view in iOS 6, the kinds of things we're going to talk about with run loop modes and touch delivery and responder chain, they are true for all of those different kinds of UI scroll views with or without OpenGL. So stick around. And then at the end, actually, we're going to get into something that was new in iOS 5 last year, but that we didn't have time to talk about. And that's controlling the stop offset of your scroll view after the user scrolls and it comes to decelerate.

When your user scrolls a little bit and lifts their finger while it's still moving, the scroll view comes to rest. The scroll view has decided where it will land. And with a new delegate method introduced in iOS 5, it actually tells you where that will be before the deceleration starts. But even better is you can adjust that offset, which lets you do some interesting things.

So let's get started with the quick review of UIScrollView and configuration. So the basics of UIScrollView, obviously, are that you have more content than can fit on any one screen, and you want to allow your users to scroll around within it. And you do that by just setting one property on the UIScrollView, and that's your content size. And the content size is just the width and height of your content. And when you set that property, UIScrollView knows exactly how much it can scroll in any direction. And it all just works with just that one property set.

But sometimes you need to know which part of the content is currently visible programmatically during the execution of your application. And the way that we refer to that in UIScrollView is as the content offset. The 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 where we only have vertically scrolling content, the distance from the top to the point that was visible is your content offset. Now, ScrollView supports scrolling, but it also supports zooming. And to add zooming to your ScrollView, there's just two things that you have to add on top of that. First, you have to have the content that you want to have zoomed as a subview of your UI ScrollView and return that from a delegate method called viewForZoomingInScrollView. That's the first thing. The second thing is to set two properties, the minimum and maximum zoom scale, that tell the ScrollView how much the content is allowed to be zoomed either in or out. And with those two things done, your users can pinch to zoom in and out on the content, or you can programmatically control the zoom scale. So it's pretty easy. The basics are really simple. If you're not already familiar with those things, or if any of that is a little surprising, I recommend that you watch some of the previous sessions from earlier WWDCs, or check out the documentation at developer.apple.com. There's a lot more there about the basics, but today I just want to get into some of the more interesting advanced topics that we want to talk about. So first, let's take a look back at page scrolling and that photo browser that we built in 2010. If you've seen the 2010 session, or if you've downloaded the PhotoScroller sample app from developer.apple.com, then you're already familiar with the kind of thing we're about to be talking about. But if you haven't seen it, just a quick review of what we're talking about here. You want to build the photo browser that feels just like the Photos app. Your users can scroll back and forth between photos, zoom in on them, and then scroll back from the zoomed-in photo to photos next to them. So just that exact same kind of photo browsing experience that you see on iOS.

And if you've seen the sample code or watched the session, then you're familiar with the view hierarchy that we built in order to make this happen. It looked something like this. We had an outer paging UI scroll view that was the base of our view hierarchy, and we used that for scrolling back and forth between photos horizontally. Then each page, each individual photo, had its own zooming UI scroll view. So we created one UI scroll view per page, and that was what we used to zoom in and out on the photos. And then the photos themselves were each UI image views added as subviews to those UIScrollViews.

So we spent basically an entire session talking about how you put all this together. And there was really, there's quite a lot of code behind it. And if you've looked at the sample code, you're probably familiar with that. So the great news with iOS 6 is that that bottom part, the paging UI scroll view, if you're using UI page view controller instead, that can go away entirely. UI page view controller takes care of that entire part of this view hierarchy for us.

So now we're left with a little bit less. We just have these two separate collections of views that we have to build. But you can simplify this even further now, because UIPageViewController is a view controller. And view controllers are really great for building screenfuls of content. And really, each one of these photos is a whole screenful of content.

So all we have to really think about now is building a view controller that knows how to zoom and display a single photo, rather than having to worry about all of them. And once we have a view controller that can display one photo, we can just allocate many of those and keep handing them to the UIPageViewController to display as many as we want. So really all we have to do now is think about building a single zooming UIScrollView that knows how to zoom a single image view. So much simpler to conceptualize and really work on than what we had in the past.

Next, let's take a quick look at how the page view controller will ask for the photos so we know how things will behave once we have our single view controller configured. So let's put this into a UI -- or into an iPhone. Now let's assume that the user puts their finger down on the screen and starts scrolling to the next photo. At that point, UI page view controller will ask for the next photo using a delegate method, view controller after view controller. And at that point, you just allocate a new copy of that same type of UI view controller that you created to display and zoom a single photo, put your new photo in it, and return it to the page view controller. Page view controller deals with deciding when it should be added to and removed from the view hierarchy. It makes sure that it doesn't live longer than it should so that if you scroll through a whole bunch of pages, you don't end up with unbounded memory growth. It takes care of all the details of when the view should be added and removed, and you don't have to worry about any of that. And that was actually a big part of the code that we had to write before. And then similarly, when we scroll in the other direction, we'll get another delegate method, viewController before viewController, and we can just return a new instance to represent the previous photo. So it's pretty simple. With those things done, all that's really left to implement then is zooming in and out on that single picture that we have. And with the introduction of UIPageViewController, this hasn't really changed too much. So we're not really going to get into the specifics of how this works. If you're not really familiar with how to set up a zooming UI scroll view, the sample code is still available, and there's actually new sample code this year based on the new stuff. And you can go check that out, but we're not going to get too much into the details right now. But UIPageViewController, once it's zoomed in, does handle scrolling back out to photos next to the one that you were zoomed in on. So with that in mind, Eliza is going to come up and do a demo of how we can build this.

I'm an engineer on the Passbook team now. And I'm gonna show you the demo, or just the sample code that we had from the demo two years ago. I've added one new feature to it. So let me switch over here to... Okay, great. So here's the app we built two years ago. And as you can see, it pages through a bunch of photos, and you can zoom in on one of them, zoom back out. I've also added rotation to it this year. So it supports rotation, and it will continue to be centered on the same photo that you were looking at. And it will even preserve your zoom scale when it rotates. So all of that you can check out in the sample code attached to this session. What I want to do now is show you the amazing amount we went to to build this two years ago. I'm going to show that to you really quickly and then show you how easy it is to make it much simpler using the new UIPageViewController scrolling mode. So switching over to the code, this is the root view controller class.

And I'm going to just give you a really quick tour of the class. So it does three major pieces of functionality. The first thing it does is create a paging scroll view, as you can see here. And then it configures it, it positions all of the pages within it, it positions the paging scroll view's own frame to leave that extra space. And here's the code that produced that effect. All of this stuff in viewDidLoad, all of this stuff in frame for paging scroll view, frame for page at index, content size, tile pages, all of this stuff. Oops, that's actually the next part. So that's one of the pieces of functionality that this class is performing. Another thing that it's doing, as I just gave away, is it's doing all of this work to tile the pages within this outer paging scroll view. And the reason for that is that we want to make sure to never have more content loaded into memory than is needed to display what's on screen at any given time. So that was a ton of work. In fact, we spent nearly half a session two years ago just talking about how to make that tiling work. So here's the code that was responsible for that. And then finally, the last piece of functionality that this root view controller is providing is rotation support. So if you think about it, when you go to rotate this wide paging scroll view that has a lot of pages in it, each one in portrait mode is about 320 points wide. When you rotate it, suddenly each page becomes 480 points wide, which means that all of them need to move over to make room for the wider ones before them. And in addition, we need to change the content offset of that outer scroll view so that we don't end up in the middle between two different pages. So all of that work is being done here in these rotation methods where we're figuring out where we were, we're going and restoring it afterwards. So with that in mind, let's convert this to use UIPageViewController. So my first action here is going to be to take these root view controller classes and delete them because all of that functionality is provided by UIPageViewController. So now I've completely hosed my app. So let's pick up the pieces. So here in my app delegate class, I was importing this root view controller. So let's not do that anymore. And we obviously can't make one anymore. So let's make a UIPageViewController instead.

We're going to need to be the -- we're going to need something to be the UIPageViewController's data source to provide all the pages. So I'm going to stick that functionality into the app delegate class. So we'll add -- we'll conform to the UI page view controller delegate or rather data source protocol. Okay. So switching now to the app delegate.m file, we can't make a root view controller anymore. So let's get rid of that. Instead, we're going to make a UI page view controller. Now, let me get rid of this column so you can see that code better. So you make a UI page view controller by passing a few different options. So we need a transition style. The transition style we're going to choose is transition style scroll. We need an orientation for navigation, which is going to be horizontal in this case. And then we need to pass an options dictionary or optionally we could pass an options dictionary. And in this case, we're going to use that to produce the interspace paging that Josh described in his slides. So I'm going to use the new -- this is the new dictionary literal syntax that was introduced in iOS 6 that makes this really compact. So we're basically passing a dictionary that consists of one key, this incredibly long unpronounceable key, and then an NSNumber to indicate that we want 20 points of spacing between pages. So, all right. We've got our view controller. We need to set its data source. So we'll use -- we'll make the app delegate be the data source. And now we need to produce just the first page to kick things off. So I'm gonna put a to-do here. We need to kick things off by making the first page. But before we can do that, we need to actually go write the UIViewController subclass that's gonna be our page. So I'm gonna get this column to come back in and add a file. So... here we go. All right, we're gonna make a UIViewController subclass, and I'm gonna call it ImageViewController 'cause its job is to display an image.

Add that to the project. I'm just gonna move it up here so I've got it handy. Okay, so what is this ImageViewController gonna do? Turns out it's gonna do pretty little. It's gonna be really easy to write. So the first thing that I'm gonna do is declare a couple methods that this class knows how to implement. The first is a class method to return an ImageViewController for a particular page index. And then we're also gonna have these ImageViewControllers know how to vend the page index that they are displaying.

So in the implementation for this, let's get rid of some of this excess stuff. Okay, we need to import my image scroll view subclass. Now, this is the same scroll view subclass that we were using before. I'm not going to show you much about it, except to note that the class vends this image count. So this scroll view subclass is actually handling all of the logic for figuring out what image to display on any given page. So we'll just keep that just the same. So we're going to need in this image view controller class to have a page index IVAR. And then we're going to need to implement just a few methods. So we need an initWithPageIndex method just to set the page index on the new instance. We need a page index method to return the page index for a given instance. Now let's go ahead and implement this image view controller for page index method that I declared in the header. It's going to be pretty simple. What we want to do is if the page index that was passed in is a reasonable one if we actually have an image for that index, then we're going to return one of these view controllers.

If not, we're going to return nil. And you'll see in a minute that that will turn out to be really handy when we're implementing the data source methods for the UI page view controller. So if the page index is within range, if it's greater than or equal to zero and less than the image scroll view's image count, then we'll return a new instance initialized with the page index that we got asked for. And class, just two more methods -- rather three more methods to go. So we need to make a load view method. And in that method, we're going to do nothing fancy. We're just going to create one of these image scroll views, same class we were using before, set its index to be our page index, set its auto resizing mask to give it flexible width and height. This is really important. If you notice that when we initialized the scroll view, we actually didn't give it a frame. We are not responsible for sizing our view ourself. UI page view controller is going to do that for us as long as we make ourselves flexible.

So we're going to do that. We're going to set our view -- because we're a view controller here, right? So we're going to set our view to be the scroll view. And that's pretty much it. We need to then also say that we can auto rotate just to continue to support rotation. So the way that UI page view controller works is it asks its current page permission to to rotate when it itself is asked whether it's going to rotate. So we're going to say, yeah, we support all of the interface orientations. All right, so that's pretty much it for our image view controller. So as you can see, it's a lot less code than we just deleted. And now all that we have left to do is back in this app delegate, we need to import our new class that we just made, our new header. And then we need to go ahead and make this first page, as we promised. So page 0, it's just image view controller, image view for page index 0. And then we need to set this page on our UIPageViewController instance. So it's another one of these many argument methods. SetViewControllers.direction.animated .completion. So the view controllers is just gonna be an array consisting of only page 0. The direction is forwards in this case. And we don't really want to animate this, and we don't care about when it completes, because this is all being done before the app actually even visible on the screen. So great. So we've got a first page. Now, if I were to run this now, you would see the first page, but we would crash when we tried to scroll because we haven't implemented our data source methods. So we need to implement those.

We need two data source methods, page view controller, view controller before view controller, and view controller after view controller. And these are going to be incredibly fast to write because we just need to grab the page index out of the view controller we were given. And in the case where we're trying to make the one before it, we just need to subtract one and return the page for that index. So I'm going to return image view controller, image view controller for page index, the one before. This is where it becomes really handy that this method, image view controller for page index, returns nil if the page index is out of bounds because the way that you tell a UI page view controller to stop allowing scrolling is by returning nil from this method. So if the page index is out of bounds, we'll just automatically make it so scrolling comes to an end at that page. And then in the after case, we do exactly the same thing, but plus one. And now we can go ahead and run it.

And here we go. It looks just the same as before, which is a good sign. I can scroll from page to page. I can rotate and scroll back, rotate again. And let's see, I can still zoom in, zoom out. Everything's working just as before. So minus a lot, a lot of code. So back to Josh.

So hopefully that is going to be a really nice benefit for everybody and make it easier for everyone to get scroll views and photo browsers that feel just like the Photos app on iOS. The next thing that we want to talk about, I'm actually really, really excited about. I think this is very cool. We're going to talk about integrating UI scroll view into your OpenGL games and applications.

So you may wonder why you might want to do this. A common thing that you end up finding is that you've got something in your game, usually in a setup screen or a browser of some sort at the beginning of a game, where you have to be able to scroll through some content. And it's at first glance not obvious how you might use UIScrollView in order to actually scroll the content in your OpenGL games, because the way UIScrollView works is that you put the scrollable content into the UIScrollView, and UIScrollView moves it around. obviously kind of at odds with your own OpenGL stuff where everything exists in one UI view that displays all the OpenGL content. So let's take a look at how we might do this. I really wanted to get everyone really invested in this and excited and pull you all right into the experience here. So I have some really amazing graphics to really just illustrate this well. So let's assume that we've got a game that we're going to build that is going to be a racing game. And of course, in your racing game, you need to allow your users to pick which car they want to race as. And so let's imagine that we're going to do that on some screen where we have a podium, and we're going to place cars on it that the user can scroll through. So we've got this immersive 3D game with a podium that's going to display our cars. And I've drawn this myself, so you know it's good. Now let's place our car on the podium. And then we want to allow our users to scroll between different cars to pick the one that they want to race as. Now, if you were to do this in just a regular 2D game, the way you would probably do that is by taking this 3D scene, pulling the car out of it, and putting it inside a UI scroll view so that you can scroll between that. But of course in a 3D world, this might not be possible because maybe you have some other parts of the scene that interact with these cars. You might have some lights that are on top shining down on the cars that you're trying And maybe those lights are causing reflections off the windows.

It's just, it's part of the scene. You can't pull it out and put it on top. So obviously, it's not going to work the way that UI scroll view normally does. So one way that you might decide to try and approach this is to put the entire OpenGL view inside of your UI scroll view. Now, this has many different downsides. But one of the problems with it is that you actually want to limit the area that can be scrolled. What we've got here, we've got all this other stuff around the car. We might have some area at the top that we're showing how much money somebody has to spend on their car. or some buttons around on the bottom to let you actually start the game. And it really doesn't make sense for touches beginning in these areas to start scrolling. We want to just limit the scrollable area right to that center part. So putting the entire OpenGL view inside the UI scroll view doesn't really make very much sense. So we can't put our content in the OpenGL view, and we can't put the entire UI scroll view -- or, sorry, we can't put the entire OpenGL view in the UI scroll view. It kind of seems like there's no option left. But if we take a step back for a minute and think about what we actually want to get out of the UI scroll view, the goal here is to have scrolling in your OpenGL game feel the same as scrolling everywhere else on iOS. You want your users to just feel like it's a native part of the platform. It's one of those little pieces of polish that really takes a really great app and just makes it that much better. Your users know what scrolling on iOS feels like. So getting the scrolling tracking feel and the deceleration and the bounce, that's the kind of stuff we really want from UIScrollView. We don't actually care about anything about how ScrollView does with drawing because it doesn't draw anything normally. It moves other content around. So we could actually decide to use a UIScrollView only for its tracking, deceleration, and bouncing, and not let it do anything having to do with drawing at all. And the reason we can do that is because our OpenGL rendering is happening with every frame being rendered independently. So we can pull the content offset out of the UI scroll view at every frame and give it to our OpenGL rendering code, which it can then use when rendering the scene. So to build that view hierarchy, we can just place our UI scroll view on top of our OpenGL view and make it transparent so you can see through to it and it doesn't obscure your content. And we can configure things that way and then use it for tracking, deceleration, and bouncing, and not worry about the drawing. So to show us how we can build something that works just like this, Eliza is going back and do another demo.

You can see here the amazing work of my OpenGL genius. This represents the outer limit of my OpenGL competence, so be impressed. We've got a bunch of cubes here that are rotating, and we have another really big cube here that's rotating more slowly and changing color. So what we want to-- so here's what this app does out of the box. You can tap on this bigger cube and make it randomly change to a different color and a different rotation speed. You can also tap on these little cubes, and they'll take on the properties of the big cube at that moment. So this way, we can, for example, if we wait a few seconds and tap on the next one, you can see how the big cube is changing over time, which is really useful. So there we go. So now we might want to be able to track the big cube's progress over time, over a longer period of time, and so we might want to have more than just three of these little cubes to record its progress. Okay, so we might want to be able to actually add more cubes and then we could scroll through them. So to do this, we can use the trick that Josh just described of placing a scroll view inside the OpenGL view to accept touches, and then we're going to set a new content offset on the OpenGL view to change where the little cubes are being displayed. So let me move over to the code. And basically, we've got a view controller here, which is a subclass of GLK view controller, which is part of the standard setup for making an OpenGL app on iOS. And then we have almost all of the work is being done in this cube view class, which is essentially drawing all that stuff. It's being updated at every frame by the OpenGL rendering machine. And then it also vends a few properties that are going to be useful when we want to set up this scroll view.

So it vends a scrollable frame, which is the frame in the OpenGL view that we want to make scrollable. And it also gives us a scrollable content size telling us how much room there is within that scroll view for scrolling. It also has a read/write property, scroll offset, that we'll be able to set when the scroll view scrolls so that the cube view will update its view accordingly. All right, so that's the setup. Let's go ahead and do it. So here in my viewDidLoad method, I'm going to add a scroll view. I'm going to set the scroll view's frame to be this scrollable frame vended by our cube view, and I'm going to set its content size to be the scrollable content size.

All right, I'm going to make the indicator style white here, and that's just for illustration purposes. I want you to be able to see where the scroll view is and that it's scrolling when I switch over to the demo. We'll take that out in a little while. And finally, I'm going to make this scroll view be a subview of my view, which is the cube view. All right, so we've got the scroll view now, and it's all configured. The last thing that we need to do is make sure that we can find out when the scroll view scrolls so we can set a new content offset on our OpenGL view. So for that reason, I'm going to make myself the scroll view's delegate. I need to declare conformance to appease Xcode. And then I'm going to implement a single UI scroll view delegate method, scroll view did scroll. And in scroll view did scroll, I'm going to read the new content offset off of the scroll view and just set that on my cube view. So that will update the cube view's model so that the next time it goes and renders, it will render with a new scroll offset. That's pretty much it, so we can go ahead and run this.

All right, so we've got our cubes. Everything seems to be going as planned. Now let me go ahead and scroll. OK, well, mixed success. We've got a scroll indicator that can show you that we're really scrolling here. The scroll view's in the right place, at least. But not only are we not seeing the little cubes scroll, we're not even actually seeing any animation take place whatsoever.

Everything has just basically completely broken. So let me tell you why everything completely broke. And for that, I'm going to take a little journey aside into the world of run loops. So here's how an OpenGL view works. At every frame, it renders a new version of its scene. And the way that it finds out that it should render another version of the scene is there's a heartbeat timer, a special kind of timer, that fires at the screen's refresh rate. And each time it fires, the GLK view is told to display again. Now, timers on iOS are run loop sources. They're scheduled in a run loop. And everything that's scheduled into a run loop is scheduled to-- the source is scheduled to run in a particular run loop mode. And as it happens, the default run loop mode for this type of timer is the default run loop mode, which is the mode that run loops are almost always running in. But scroll view has a surprising and unusual property, which is that when the scroll view is tracking, when either your finger is down and you're moving or it's decelerating to a new position, it runs the run loop in another mode called UI tracking run loop mode. The effect of this is that while the scroll view is tracking, the timer that's supposed to be updating our OpenGL view is actually not firing, which causes the Open GL view to stop displaying. And it also means that we don't see any scrolling happen because the scrolling needed to happen because we're re-displaying. So in order to fix this, we're going to need to make it happen that this OpenGL view continues to update its display, even when the run loop is running in UI tracking run loop mode. So I'm going to show you how to do that. So back to the code here, what we need to do is create a timer that's just like the timer that's been created for us by the GLK view controller. So that type of timer is called a CA display link. So we're gonna make one, and I'm gonna do that down here. So we're gonna have a couple methods that are responsible for starting and stopping the CA display link. So when we start it, we need to make one if we don't already have one. So if we don't already have one, we're gonna create one, and we're gonna give it as a target our cube view, and as a selector to invoke this display method, which is the same method that is already being called on the cube view to force it to re-render.

So that's gonna happen at frame rate once we schedule this in the run loop. So we schedule into the run loop by adding it to the main run loop, and we're gonna add it in this UI tracking run loop mode. So this will cause this display link to fire when we're in tracking mode, and the other display link that was already firing will fire when we're not. All right, so that's all we need to do to start it, and then to stop it, we'll just nil it out, invalidate it and then nil it out. All right, so let's actually call these methods at the appropriate time. So we wanna start the display link as soon as tracking starts on the scroll view. So in scroll view, we'll begin dragging another delegate method that we hadn't implemented before. We're just gonna call startDisplayLink if needed. And then we wanna stop the display link as soon as the scroll view has either finished decelerating or has stopped tracking without deceleration.

And so there's these two different paths that we actually have to make sure we cover. So the first one is the user lifts up their finger and at that point there's either momentum or not. If there's momentum, then it's gonna start decelerating and will decelerate will be yes. If there is not momentum, then the scroll view will not decelerate and it will just stop tracking at that moment. So in the case where there was momentum, we'll find out that the scroll view stopped decelerating in this scroll view did end decelerating method.

All right, so in that case, we wanna just stop the display link unconditionally, 'cause that means we're no longer animating any motion. In the case where we ended dragging, we want to only stop it if we're not about to decelerate. If we're decelerating, we wanna keep the display link going because the run loop will continue to run in UI tracking run loop mode. All right, so with that in place, oh, let me actually remove my white indicators now, and I'll also actually hide the scroll indicator altogether better. Okay. So let's go ahead and run it again. All right. So we've got our cubes. First things first. All right. Scrolling now actually works. Yay. And we can now finally -- all right. I'm going to randomize my big cube here. Okay. We've got it turning blue. Now I'm going to go ahead and turn all of these cubes blue, which is my dream. So -- uh-oh.

All right, my clicking is broken. So in fact, I've actually broken taps. So as you can see, taps are actually still working in the rest of the app. So I haven't broken them altogether. But in the area where the scroll view is, taps are not making it through to my OpenGL view. So to explain why that's going wrong, I'm going to bring Josh back up.

There's actually one other mode that Eliza didn't mention, and that's the NSCommonRunLoopModes. Now, NSCommonRunLoopModes is actually a combination of the other two modes, the default and the tracking run loop mode. So it's one constant, NSCommonRunLoopModes, that actually encompasses both. So if you didn't want to schedule in just one or the other, but wanted to schedule for both, you could just use NSCommonRunLoopModes. Now, Eliza mentioned all of this in the context of OpenGL views, but what she didn't mention is that this actually affects all UIScrollView subclasses everywhere, such as UITextView, UIWebView, the new UICollectionView, all of these things. So if you're using any scroll view anywhere, you may have run into this in the past. One common example of other places that you'll see this are places where you're using NSTimers. The way that you would normally schedule a timer, the most convenient way, is to call scheduledTimerWithTimeInterval targetSelectorRepeats. And what this will do is actually schedule this timer on the run loop on your behalf. But what it does is schedule it in the default run loop mode. So if you schedule a timer like this for one second from now, and one second from now the user is scrolling a scroll view, that timer's not gonna fire at the time you expected. So you may have actually come across this and not been entirely clear on what was happening. That's why it happens. So you can work around this with nstimer in particular by creating your timer using the other class method, The timerWithInterval method takes all the same parameters, but it doesn't get scheduled into the run loop automatically. Now, the reason you'll do that is because then you can use the NSRunLoop method to schedule the timer yourself in a particular mode. So we can call NSRunLoop main run loop, add timer for mode, and pass in, in this case, NSCommon run loop modes to indicate that we want this timer to fire regardless of whether we're in the default or tracking run loop mode.

So that's one common case. Another one then is the performSelectorAfterDelay method. performSelector with object after delay also schedules into the default run loop mode. So if you call this method, even though it's not necessarily obvious that it's having anything at all to do with run loops, if your user is scrolling one second from now, the selector that you asked to perform isn't gonna get called. This one's actually even easier to fix in that there's just a slightly longer version of the same method called performSelector with object after delay in modes. Now, it's slightly different in that this actually takes an array of modes. So here we've used the new array syntax introduced in iOS 6 to have a NS common run loop modes as the single element in our NSArray. So if you happen to find that when you're using your UI scroll view, something that you think should be happening right now isn't happening until the scroll view finishes, take a look at your run loop modes 'cause that's probably what's going on. All right, so that's run loop modes. Now let's see if we can't help Eliza realize her dreams. So UI Responder and Event Delivery. really the foundation of the basics of touch handling, ignoring UI gesture recognizer for a minute, on iOS.

So let's take, for an example, a really simple case, ignoring the OpenGL view and the UIScrollView. Let's just assume we have some plain UIView, not a subclass of anything, and that this thing has one child UIView. So we'll put that right in there. And now let's let our user put a finger down inside of that child view. So the way event delivery on iOS works is we hit test from the window out towards the user and find the deepest view that we would have hit, in this case the child view, and we deliver the touch to that view. So touches began with event will get called on that child view. If you don't implement touches began with event, and it will automatically get forwarded by the UI responder methods up the responder chain. And the next responder from a view is its super view. So it would get forwarded to the parent UI view. Now if the parent view didn't implement touches began with event, the responder chain would forward to the next responder. And the next responder in this case would actually be -- because let's assume that this parent view has a view controller would be the view's view controller. So child to parent to that parent's view controller.

Now if the parent view controller didn't handle it, the next responder is the UI window, because there's no other super view. And if the UI window didn't handle it, it would go to the UI application object. And finally, if it's not handled there, it would go to the UI application delegate that you would have set on your application. So that would be the normal responder chain path, and any of these views would have seen that touch if one of them didn't handle it. But clearly that's not what's happening in Eliza's case, because the parent view, which in her case was her NSOpenGL, or sorry, her OpenGL view, that subclass of UIView, is where she implemented her touch handling logic. So, according to this diagram, her parent view, that OpenGL view, should have received the touch. So to understand why it didn't, we have to take a look at the actual view hierarchy, which is a UIScrollView embedded on top of an NSOpenGL, keep saying NSOpenGL, on top of an OpenGL view. So let's do the same thing here and have the user put a finger down in that UI scroll view.

Of course, we're going to deliver it right to the UI scroll view first. But the problem in this case is that that's actually where this responder chain ends delivery. Because UI scroll view does implement touches began with event and doesn't forward it up the responder chain. So it stops right there, and her OpenGL view never sees it.

So obviously we'll have to do something in order to figure out how her OpenGL can get this touch because that's where her touch handling logic lives. Now, one thing that I've seen a number of times that I really, really, really wanna discourage is something that looks like this.

You implement touchesBeganWithEvent in your subclass of UIScrollView and manually call touchesBeganWithEvent on some next responder or some other object. Now, this is really, really not supported. We don't support forwarding touches manually between views that are owned by UIKit. UIKit can no longer reason about the flow of events once you've started manually forwarding views -- manually forwarding touches. It's just -- it leads to bad news.

It would actually turn your view, or your responder chain into something that looked like this where you kind of manually forwarded it to the OpenGL view, but because UIKit didn't know you were doing it, it would stop there and not get forwarded on the rest of the chain anyway. It really isn't what you want.

So that's not gonna work. We could try one other thing, which is to put the OpenGL view inside of that UI scroll view because that would fix the responder chain. Our OpenGL view would start getting the events then, but as we discussed earlier, we don't want this view configuration because we want our scroll view to only be on a small part of that OpenGL view. So once again, it seems like we've kind of gotten to an impasse. What can we possibly do? We can't have the OpenGL view in the scroll view, and we can't have the scroll view in the OpenGL view.

Well, this is another place where we have to stop and think a little bit differently than we would in any other non-OpenGL application. And again, it goes back to the fact that we don't actually want UIScrollView to do any of the drawing. All we're trying to do is get its event tracking, its deceleration, and its bouncing. We just want the behaviors that UIScrollView is providing.

So really, we don't even need the scroll view to be in the view hierarchy visible at all because we're not relying on it drawing anything. So we could just try and hide it or move it off to the side or add it as a subview somewhere where you can't even see it and it wouldn't actually change our visuals at all. The only problem we'd have left then is that, well, obviously it's not gonna get any touches if it's not visible and on screen. So we would have to come up with some way to make sure that it's still got touches if we were to try and do this. And it turns out that we actually can do exactly that. So let's put a dummy UIView in its place just to provide the sizing exactly where we had had it in the view hierarchy before. And this dummy view can just be a plain UI view. It doesn't have to be a subclass at all. The most important property of this view being that it doesn't implement touchesBegan, moved, ended, or canceled. And the reason for that is that we've now fixed our responder chain. We've got a child view, and it will forward events to the OpenGL view if it doesn't handle them, which it won't, because we're not going to have it.

So we've got the responder chain back in order, but we still don't have our UI scroll view receiving any touches. But we can fix that by taking advantage of the fact that UI scroll view does its touch tracking using UI gesture recognizers. So UI scroll view has a number of UI gesture recognizers attached to itself. And those gesture recognizers are exposed through properties on the UI scroll view. So we can grab them and move them to another view. Once we've attached them to that child view, they're in the view hierarchy in a place where we want them to receive touches, and so they will start receiving touches. When those gestures recognize, they'll notify the UIScrollView that they have been recognized, and the UIScrollView will begin scrolling despite the fact that it's not actually visible on screen. And really, this will let us implement exactly what Eliza was trying to do. So in order to show how that works, she's gonna come back up and do one more demo.

Okay, this is going to be a really easy demo. We just need to replace our scroll view with a dummy view and move the gesture recognizers over. So the first thing I'm going to do is move the scroll view out of the way so that it stops trying to usurp all my touches. And I can do that by moving it aside, by sticking it under everything, but probably the easiest way to do that is just to hide it because hidden views don't get hit test. So I'm going to go ahead and hide it. And now I'm going to add a dummy view. I'm going to make the frame of the dummy view be the same scrollable frame, because that's where we want to be able to detect our panning gesture.

I'm going to add the gesture recognizers from the scroll view. So here I'm accessing the scroll view's pan gesture recognizer property. I'm just taking that gesture recognizer and I'm moving it over to the dummy view. I only care about the pan gesture recognizer in this case because I'm not trying to allow zooming on my view. And then finally I'm going to add this dummy view as a subview of my OpenGL view. And that's pretty much it. So that ought to solve our problem. So here we are scrolling. So I can still scroll, first of all, which is pretty cool, because the scroll view is actually not even receiving touches now. And then I can actually now tap these views and it works. So this actually does solve the problem. Now, it solves it in an odd way, right? We have this view here that's kind of performing this weird function and it's a, I don't know, It's a little roundabout. So I want to just mention that under some configurations, you wouldn't actually have to do this. So as it happens, my cube view is detecting touches using touches began, moved, ended, and canceled, which makes it vulnerable to this responder chain problem. So we wanted to show this because we think that people who already have existing OpenGL apps and have already got their touch handling written might want to be able to adopt this method. But if you were writing an app from scratch and you decided to use gesture recognizers for your touch handling, then you wouldn't face the problem that I faced at the end of the last demo, because gesture recognizers don't depend on the responder chain to recognize. So had I been detecting taps using a UITap gesture recognizer, we wouldn't have even had this problem in the first place. So keep that in mind when you're -- if you're adopting this technique from scratch. In the case where you're using tap gesture recognizers to do touch handling, then you could just leave the scroll view there, not hide it, not have a dummy view and everything. All right, so turning it back over to Josh for one more topic.

All right. Thanks, Eliza. So the last thing we want to talk about-- just kidding. I don't know why they put that button on there-- is deciding where your scroll view will come to rest after you've scrolled and it started decelerating and moved with some initial velocity. So the idea is that we've got this great implementation where we can pick different cars, which is really awesome, but we don't want to allow the user to scroll just partway between two cars and land in some intermediate spot. We want to make sure that no matter where they scroll and lift their finger, they always land right on a real whole car centered right on our podium. So one way that you could solve this is by turning on the paging mode of UIScrollView. The paging mode will make sure that you're always scrolling one page at a time and that things will land centered in the right spot. Now, the downside of that, and the reason you might not want to do it, is because the paging mode will only let you scroll one page at a time. Maybe we want to allow the user to scroll really quickly, make a really big gesture, and move through many, many cars all at once, but still land at the end with one centered right on that podium. Now this is exactly what the new delegate method introduced in iOS 5 is for. But it may not be initially obvious that that's really why it's there. So let's take a look at it. The delegate method is scroll view will end dragging with velocity target content offset. So it's maybe not entirely clear how this would even do what I just mentioned, but the The key to it is that last parameter, the target content offset. And the reason is because that parameter is actually an in/out parameter. It's a pointer to a CG point. On input, when this method is called on your delegate, it contains the point that the scroll view plans to decelerate to. But you can change it. And if you do, then on return, the scroll view will adjust itself and decelerate to the new point that you return. It is kind of possible to abuse this and sort of return places that make it shoot off and directions or something, but porting ease don't do that. Just adjust it a small amount and make it land in a reasonable place, but not provide surprising behavior to your users. So the way that you would do this then is just to implement this method and modify that target content offset parameter. Here I've written just a really simple method, closest car offset, which let's just assume will round to the closest offset given the one that I've passed in. And by modifying the output parameter there, that's all we have to do. And our scroll view will come to rest at exactly the right spot. So Eliza is going to come back and add that to our demo application.

So let's add this. So in this case, let me switch back to the application running. We don't have any cars, because I couldn't possibly draw one in OpenGL. But let's make it so that every time that you scroll this, you end up perfectly centered with three cubes in the middle of the screen, so that this configuration isn't a possible resting point for the scroll view, just to demonstrate the concept. So in order to do that, we're going to add a new method that this cube view implements. I'm calling it scroll offset for proposed offset. So what the cube view is going to do is go look at the offset that was proposed, figure out what's the nearest one that would get three cubes perfectly centered on the screen, and return that. So let's go ahead and implement that. So switching to my implementation, I'm going to add it here under my scrollable content size method. So in this scroll offset for proposed offset method, the first thing that I'm going to do is calculate how much have we overshot -- so basically you imagine that here's where you'd be if the three cubes were perfectly centered, but in fact the user has dragged the scroll view over to there. So we want to calculate how much did we overshoot the perfect offset. And I'm doing that by taking -- I'm modding the actual offset X by the width of one of my little cubes to see how far into a little cube did we get. So now that I've calculated that overshoot, I'm going to see, well, did we overshoot by less than half or more than half of a little cube? Because if it's less than half, we want to round down, and if it's more than half, we're going to want to round up. So if the overshoot is less than half of a little cube width, then we will make our offset X -- we'll subtract the overshoot from the offset.

If it was more than half of a little cube width, then we'll add on the rest of that little cube width. So that's the little cube width minus the overshoot. And then we're just going to return the new offset that we calculated. All right. So let's call that from our view controller. Once again, we're going to add a final view controller delegate method, the one that Josh just mentioned. Let me give us more room. So in the scroll view will end dragging with velocity target content offset method, we're going to take the target content offset here that you can see at the end. And we're going to pass that to our cube view as the proposed offset. So we're going to say the new target content offset that we're going to return via this in-out parameter is the result of passing the one we were given to this scroll offset for proposed offset method. And that's pretty much it. So if I run this now, you can see that when I scroll, it sort of decelerates slowly to the exact right point. If I even were to drag a little way into a cube and let go with no momentum, it would still come back to the page boundary that we wanted. So that's a pretty useful method that I think is cool that got added back in iOS 5. So that's pretty much it for my demos, and I'm going to turn it back over to Josh to finish up.

All right, well, thanks again for coming. If you have any more questions about any of the content on the session here, Jake Behrens is the UI Frameworks evangelist. If you want any more documentation about ScrollView, of course, that's the ScrollView link at developer.apple.com and obviously the dev forums. There's a couple of related sessions later still this week, the Introducing Collection Views session. If you haven't seen the new collection views introduced in iOS 6, this stuff is pretty cool. And you can learn all about it in the repeat. Thursday at 9:00 AM. And then Building Advanced Gesture Recognizers is also tomorrow, Thursday at 11:30 AM, where we'll talk more about awesome things you can do with gesture recognizers, if you haven't seen a lot about that yet. There's also a ScrollView lab tomorrow morning, first thing in the morning, 9:00 AM. So if you have more ScrollView questions, come see us there. Thanks for coming.