Enhancing User Experience with Scroll Views

Session 223 WWDC 2012

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.

Josh: Good afternoon everybody. Thanks for coming back to another session about UIScrollView. For those of that have come in previous years, welcome back. 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. 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. That's just going to be a couple minutes, and then we'll get into a lot of more interesting things. First of, we're going to start about just a little bit of review about how to configure your UIScrollView and how UIScrollView's 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. The reason we want to review this a little bit more today is because what the introduction of a scrolling mode on UI page view controller in iOS 6, building this photo browser has come 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. That's integrating UIScrollView into your OpenGL games and applications. If you're sitting there thinking, "I don't have an OpenGL game or an app, so maybe I'm just going to get out when that comes up." 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 UIScrollViews. If you use UIScrollView, UITableView, UITextView, UIWebView or even the new UICollectionView 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 UIScrollViews with or without OpenGL, so stick around. 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. 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 the new delegate method introduced in iOS 5, it actually tells you where that will be before the deceleration starts. Even better is you can adjust that offset which lets you do some interesting things. Let's get started with the quick review of UIScrollView and configuration. The basics of UIScrollView obviously are that you have more content than can fit on anyone's screen, and you want to allow your users to scroll around within it. You do that by just setting one property on the UIScrollView and that's your content size. The content size is just the width and height of your content. When you set that property, UIScrollView knows exactly how much it can scroll on any direction, and it all just works with just that one property set. Sometimes, you need to know which part of the content is currently visible programatically during the execution of your application. 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 scroll view's frame. 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, scroll view support scrolling, but it also support zooming. To add zooming to your scroll view, 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 sub view of your UIScrollView, and return that from a delegate method called, "View for zooming in scroll view. That's the first thing. The second thing is to set two properties, the minimum and maximum zoom scale that tell the scroll view how much the content is allowed to be zoomed either in or out. With those two things done, your users can pinch to zoom in and out on the content or you can programatically control the zoom scale. 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 WWDC's 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 advance topics that we want to talk about. First, let's take a look back at Paged Scrolling and that photo browser that we built in 2010. If you've seen the 2010 session or if you've downloaded the photo scroller sample app from developer.apple.com, then you're already familiar with the kind of thing we're about to be talking about. 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. Just that exact same photo browsing experience that you see on iOS. 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 UIScrollView that was the base of our view hierarchy and we use that for scrolling back and forth between photos horizontally. Then each page, each individual photo had its own zooming UIScrollView. We created one UIScrollView per page and that was what we used to zoom in and out on the photos. Then the photos themselves where each UIImageViews added a sub-views to those UIScrollViews. We spent basically an entire session talking about how you put all these together. There's quite a lot of code behind it. If you've looked at the sample code, you're probably familiar with that. The great news with iOS 6 is that, that bottom part, the paging UIScrollView, if you're using UIPageViewController instead, that can go away entirely. UIPageViewController takes care of that entire part of this view hierarchy for us. 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 screen fulls of content. Really, each one of these photos is a whole screen full of content. 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. Once we have a view controller that can display one photo, we can just allocate many of those and keep panning them to the UIPageViewController to display as many as we want. Really, all we have to do now is think about building a single zooming UIScrollView that knows how to zoom a single image view. Much simpler to conceptualize and really work on than what we had in the past. The other thing that we spent a lot of time talking about in 2010 and that there's actually quite a bit of code devoted to in the photo scroller sample, and not just quite a bit of code, but slightly confusing and unclear code I guess is putting extra space between those photos. It'd be really nice if they weren't butting each other like they are in this picture right now, because these greens are just blending together. It'd be nice if you had some black space between them to just give the user a good sense of where one photo ends and the next one begins. We just like to space them out just a little bit. It really was, let's say a little bit of a hack the way it was done in the original photo scroller sample. It's much cleaner now. In fact, with UIPageViewContoller, not only is it almost no code, but it's really just one property. When you create a UIPageViewController, you use this new key, UIPageViewControllerOptionInterPageSpacingKey, and they'll still be hard because you can't remember that, but that's what completion is for. That will let you specify exactly how much space you want between those photos and it's ... really it's as easy as just passing one option when creating the page view controller. That's how we're going to configure it. Next, let's just take a quick look at how the page view controller will ask for the photos, so that we know how things will behave once we have our single view controller configured. Let's put this into a iPhone. Now, let's assume that the user puts their finger down the screen and start scrolling to the next photo. At that point, UIPageViewController will ask for the next photo using a delegate method, view controller after view controller. At that point, you just allocate a new copy of that same type of UIViewController 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 and make 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. You don't have to worry about any of that. That was actually a big part of the code that we had to write before. Then similarly when we scroll in the other direction, we'll get another delegate method, view controller before view controller and we can just return a new instance to represent the previous photo. 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. With the introduction of UIPageViewController, this hasn't really changed too much. 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 UIScrollView, that sample code is still available, and there's actually new sample code this year based on the new stuff. You can go check that out, but we're not going to get too much into the details right now. UIPageViewController, once it's zoomed in does handle scrolling back out to the next ... Photos next to the one that you were zoomed in on. With that in mind, Eliza's going to come up and do a demo of how we can build this. Eliza: Hi. I'm Eliza. I'm an engineer on the passbook team now. I'm going to 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. Let me switch over here to ... Okay, great. Here's the app we built two years ago. 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. It supports rotation and it will continue to be centered on the same photo that you are looking at, and it will even preserve your zoom scale when it rotates. 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 of effort we went to, to build this two years ago. I'll 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. Switching over to the code, this is the RootViewController class. I'm going to just give you a really quick tour of the class. 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. Here's the code that produced that effect. It's all of these stuff and you did load, all of these stuff in frame for paging scroll view, frame for paging index, content size, tile pages, all of these stuff, oops, that's actually the next part. 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. The reason for that is that we want to make sure to never have more content loaded into memory than as needed to display what's on screen at any given time. 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. Here's the code that was responsible for that. Then finally, the last piece of functionality that this RootViewController is providing is rotation support. 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. 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. All of that work is being done here in this rotation methods where we're figuring out where we were, we're going and restoring it afterwards. With that in mind, let's convert this to use UIPageViewController. My first action here is going to be, to take this RootViewController classes and delete them, because all of that functionality is provided by UIPageViewController. Now, I've completely host my apps. Let's pick up the pieces. Here in my app delegate class, I was importing this RootViewController so let's not do that anymore and we obviously can't make one anymore. Let's make a UIPageViewController instead. We're going to need something to be the UIPageViewController's data source to provide all of the pages. I'm going to stick that functionality into the app delegate class. We'll conform to the UIPageViewController delegate or rather data source protocol. Switching now to the AppDelegate.m file, we can't make a RootViewController anymore, so let's get rid of that. Instead, we're going to make a UIPageViewController. Now, let me get rid of this column so you can see that code better. You make a UIPageViewController bypassing a few different options. 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. Then we need to pass an options dictionary or optionally we could pass an options dictionary. In this case, we're going to use that to produce the inter space paging that Josh described in his slides. This is the new dictionary literal syntax that was introduced in iOS 6 that makes this really compact. We're basically passing a dictionary that consist of one key, this incredibly long and pronounceable key. Then an NS number to indicate that we want 20 points of spacing between pages. All right, we've got our view controller. We need to set its data source. We'll make the app delegate view the data source. Now, we need to produce just the first page to kick things off. I'm going to put it to do here. We need to kick things off by making the first page. Before we can do that, we need to actually go right the UIViewController sub-class that's going to be our page. I'm going to get this column to come back in and add a file. Here we go. We're going to make a UIViewController sub-class and I'm going to call it, "Image View Controller," because its job is to display an image. We'll add that to the project. I'm just going to move it up here, so it got it handy. What is this image view controller going to do? It turns out it's going to do pretty little. It's going to be really easy to write. The first thing that I'm going to do is declare a couple methods that this class knows how to implement. The first is a class method to return an image view controller for a particular page index. Then we're also going to have these image view controllers know how to vend to the page index that they are displaying. In the implementation for this, let's get rid of some of this excess stuff. We need to import my image scroll view sub-class. This is the same scroll view sub-class 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. The scroll view sub-class is actually handling all the logic for figuring out what image to display on any given page. We'll just keep that just the same. We're going to need in this image view controller class to have a page index I bar, and then we're going to need to implement just a few methods. We need to nit with page index method and just to set the page index on the new instance. We need a page index method to return the page index for 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. 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 UIPageViewController. 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. Otherwise, we'll return nil. All right, we're almost done with this class. Just two more methods, rather three more methods to go. We need to make a load view method. 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 noticed that when we initialized the scroll view, we actually didn't give it a frame. We are not responsible for sizing our view ourself. The UIPageViewController is going to do that for us as long as we make ourselves flexible. We're going to do that. We're going to set our view. We're a view controller here, right? 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. The way that UIPageViewController works is, it asks its current page permission to rotate when it itself is asked whether it's going to rotate. We're going to say, "Yeah, we support all of the interface orientations." That's pretty much it for our image view controller. As you can see, it's a lot less code than we just deleted. 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. Then we need to go ahead and make this first page as we promised. Page zero, it's just image view controller, image view controller for page index zero. Then, we need to set this page on our UIPageViewController instance. It's another one of these many argument methods. Set view controller's direction animated completion. The view controllers is just going to be an array consisting of only page zero. The direction is forwards in this case, and we don't really want to animate this or we don't care about when it completes, because this is all being done before the app is actually even visible on the screen. Great, we've got it 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. 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. 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. 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. 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 of the page index is out of bounce. The way that you tell a UIPageViewController to stop allowing scrolling is by returning nil from this method. If the page index is out of bounce, we'll just automatically make it so scrolling comes to an end at that page. Then, in the after case, we do exactly the same thing, but plus one. Now we can go ahead and run it. 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 is working just as before. Minus a lot and lot of code. Back to Josh. Josh: 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 UIScrollView" into your OpenGL games and applications. 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 set up screen or a browser of some sort at the beginning of a game, where you have to be able to scroll through some content. At first glance, it's not obvious how you might use UIScrollView in order to actually scroll the content in your OpenGL games. The way UIScrollView works is that you put the scrollable content into the UIScrollView and UIScrollView moves it around. That's obviously kind of an odds with your own OpenGL stuff where everything exists in one UIView that displays all the OpenGL content. 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. I have some really amazing graphics to really just illustrate as well. Let's assume that we've got a game that we're going to build that is going to be a racing game. Of course in your racing game, you need to allow your users to pick which car they want to race as. 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. We've got this immersive 3D game with a podium that's going to display our cars. I've drawn this myself so you know it's good. Now, let's place our car on the podium and 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 UIScrollView so that you can scroll between that. 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 to display. Maybe those lights are causing reflections off the windows. It's just that it's part of the scene. You can't pull it out and put it on top. Obviously it's not going to work the way that UIScrollView normally does. One way that you might decide to try and approach this is to put the entire OpenGL view inside of your UIScrollView. Now, this has many different downsides. 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 were 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. 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. Putting the entire OpenGL view inside the ULScrollView doesn't really make very much sense. We can't put our content in the OpenGL view and we can't put the entire UIScrollView or sorry, we can't put the entire OpenGL view in the UIScrollView. It seems like there's no option left. If we take a step back for a minute and think about what we actually want to get out of the UIScrollView, 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. 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 scroll view does with drawing, because it doesn't draw anything normally. It moves other content around. 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. The reason we can do that is because our OpenGL rendering is happening with every frame being rendered independently. We can pull the content offset out of the UIScrollView at every frame and give it to our OpenGL rendering code which it can then use when rendering the scene. To build that view hierarchy, we can just place our UIScrollView on top of our OpenGL view and make it transparent so you can see through to it and it doesn't obscure your content. We can configure things that way and then use it for tracking, deceleration and bouncing and not worry about the drawing. To show us how we can build something that works just like this, Eliza's going to come back and do another demo. Eliza: You can see here the amazing work of my OpenGL genius. This represents the outer limit of my OpenGL competent. 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. 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. 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. There we go. Now, we might want to be able to track the big cube's progress over time over a longer period of time. We might want to have more than just three of these little cubes to record its progress. We might want to be able to actually add more cubes and then we could scroll through them. 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. Let me move over to the code. Basically, we've got a view controller here which is a sub-class of GLK View Controller which is part of the standard set up for making an OpenGL app on iOS. 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 every frame by the OpenGL rendering machine. Then, it also vends a few properties that are going to be useful when we want to set up the scroll view. It vends a scrollable frame which is the frame in the OpenGL view that we want to make scrollable. 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 will be able to set when the scroll view scrolls so that the CubeView will update its view accordingly. That's the set up. Let's go ahead and do it. Here in my view did load method, I'm going to add a scroll view. I'm going to set the scroll view's frame to be the scrollable frame vended by our CubeView, and I'm going to set its content size to be the scrollable content size. 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. Finally, I'm going to make this scroll view be a sub-view of my view which is the CubeView. 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. For that reason, I'm going to make myself the scroll view's delegate. I need to declare conformance to a piece X code. Then, I'm going to implement a single UIScrollView delegate method, scroll view did scroll. 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 CubeView. That will update the CubeView's model so that the next time it goes and renders, it will render with a new scroll offset. That's pretty much it. We can go ahead and run this. We've got our cubes. Everything seems to be going as planned. Now, let me go ahead and scroll. 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. 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. Let me tell you why everything completely broke. For that, I'm going to take a little journey aside into the world of run loops. Here's how an OpenGL view works. At every frame, it renders a new version of it seen. 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. Each time it fires, the GLK view is told to display again. Now, timers on iOS are run resources. 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. 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. Scroll view has a surprising and unusual property which is that when the scroll view is tracking, when either your finger's 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 OpenGL 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 redisplaying. 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. I'm going to show you how to do that. 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. That type of timer is called, "CADisplayLink." We're going to make one. I'm going to do that down here. We're going to have a couple methods that are responsible for starting and stopping the CADisplayLink. When we start it, we need to make one if we don't already have one. If we don't already have one, we're going to create one and we're going to give it as a target our CubeView and as a selector to invoke this display method, which is the same method that is already being called on the CubeView to force it to re-render. That's going to happen at frame rate once we schedule this in the run loop. We schedule into the run loop by adding it to the main run loop and we're going to add it in this UI tracking run loop mode. 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. That's what we need to do to start it. Then, to stop it, we'll just nil it out, invalidate it and then nil it out. Let's actually call these methods at the appropriate time. We want to start the display link as soon as tracking starts on the scroll view. In scroll view, we'll begin dragging another delegate method that we hadn't implemented before. We're just going to call start display link if needed. Then, we want to stop the display link as soon as the scroll view has either finished decelerating or has stopped tracking without deceleration. There's these two different paths that we actually have to make sure we cover. The first one is the user lifts up their finger and at that point, there's either momentum or not. If there is momentum then it's going to start decelerating and we'll 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. In the case where there was momentum, we'll find out that the scroll view stopped decelerating in the scroll view did end decelerating method. In that case, we want to just stop the display link unconditionally, because that means we were 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 want to keep the display link going, because the run loop will continue to run in UI tracking run loop mode. With that in place, let me actually remove my white indicators now and I'll also actually hide the scroll indicator altogether, so this looks better. Let's go ahead and run it again. We've got out cubes first things first. Scrolling that actually works, yehey. I'm going to randomize my big cube here. We've got it turning blue. Now, I'm going to go ahead and turn all of these cubes blue which is my dream. My clicking is broken. In fact, I've actually broken taps. As you can see, taps are actually still working in the rest of the app, so I haven't broken them altogether. In the area where the scroll view is, taps are not making it through to my OpenGL view. To explain why that's going wrong, I'm going to bring Josh back up. Josh: There's actually one other mode that Eliza didn't mention. That's the NS Common Run Loop Modes. Now, NS Common Run Loop Modes is actually a combination of the other two modes, the default and the tracking run loop mode. It's one constant NS Common Run Loop Modes, that actually encompasses both. If you didn't want to schedule in just one or the other, but wanted to schedule for both, you could just use NS Common Run Loop Modes. Now, Eliza mentioned all of this in the context involving OpenGL views. What she didn't mention is that this actually affects all UIScrollView sub-classes everywhere, such as UITextView, UIWebView, the new UICollectionView, all of these things. 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 scheduled timer with interval, with time interval. Target selector repeats. What this will do is actually schedule this timer on the run loop on your behalf. What it does is schedule in the default run loop mode. 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 is not going to fire at the time you expected. You may have actually come across this and not been entirely clear on what was happening, that's why it happens. You can work around this with NSTimer in particular by creating your timer using the other class method, the timer with interval 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. We can call NSRunLoop main run loop, add timer for mode and passing in this case NS Common 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. That's one common case. Another one then is the perform selector after delay method. Perform selector with object after delay. Also schedules into the default run loop mode. 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 going to get called. This one is actually even easier to fix and that there is just a slightly longer version of the same method called, "PerformSelector with object in after delay in modes." Now, it's slightly different and that this actually takes an array of modes. Here reveals the new array syntax introduced in iOS 6 to have a NS Common Run Loop Modes as the single element in our NS array. If you happen to find that when you're using your UIScrollView, 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 because that's probably what's going on. That's run loop modes. Now, let's see if we can help analyze or realize your dreams. UIResponder and Event Delivery. This is really the foundation of the basics of touch handling ignoring you or just to recognize it for a minute on iOS. 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 and not a sub-class of anything, and that this thing has one child UIView. I'll put that right in there. Now let's let our user put a finger down inside of that child view. 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. We deliver the touch to that view. Touches began with event will get called on that child view. If you don't implement touch to begin with event, and it will automatically get forwarded by the UIResponder methods up the responder chain. The next responder from a view is it super view. We get forwarded to the parent UIView. Now, if the parent view didn't implement touches begin with event, the responder chain would forward to the next responder. The next responder in this case would actually be ... Because let's assume that this parent view has a view controller, would be that views view controller. 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. 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. 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. Clearly, that's not what's happening in Eliza's case, because the parent view which in her case was her NS OpenGL, or sorry, her OpenGL view that sub-class a UIView. It's where she implemented her touch handling logic. According to this diagram, her parent view that OpenGL view should have receive the touch. 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 NS OpenGL. I keep saying NS OpenGL, on top of an OpenGL view. Let's do the same thing here and have the user put a finger down in that UIScrollView. Of course, we're going to deliver it right to the UIScrollView first. The problem in this case is that that's actually where this responder chain ends delivery. UIScrollView does implement touches begin with event and doesn't forward it at the responder chain. It stops right there and her OpenGL view never sees it. 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, want to discourage is something that looks like this. You implement touches began with event in your sub-class of UIScrollView and manually call touches began with event on some next responder or some other object. Now, this is 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 started manually forwarding views ... Manually forwarding touches. It's just it leads to bad news. It would actually turn your responder chain into something that looks like this where you 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. That's not going to work. We could try one other thing which is to put the OpenGL view inside of that UIScrollView because that would fix the responder chain. Our OpenGL view would start getting the events then. 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. Once again, it seems like we've 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. This is another place where we have to stop and think a little bit differently than we would in any other none OpenGL application. 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. Really, we don't even need the scroll view to be on the view hierarchy visible at all, because we're not relying on it drawing anything. We could just try and hide it or move it off to the side or add it as a sub view somewhere where you can't even see it. It wouldn't actually change our visuals at all. The only problem we have left then is that obviously it's not going to get any touches if it's not visible and on screen. We would have to come up with some way to make sure that it still got touches if we were to try and do this. It turns out that we actually can do exactly that. 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. This dummy view can just be a plain UIView. It doesn't have to be a sub-class at all. The most important property of this view being that it doesn't implement touches began moved and that they're canceled. 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. We've got the responder chain back in order, but we still don't have our UIScrollView receiving any touches. We can fix that by taking advantage of the fact that UIScrollView does its touch tracking using UIGestureRecognizers. UIScrollView has a number of UIGestureRecognizers attached to itself. Those gesture recognizers are exposed through properties on the UIScrollView. We can grab them and move them to another view. Once we have attached them to that child view, they're in the view hierarchy and a place where we want them to receive touches, and so they will start receiving touches. When those gesture's recognized, they'll notify the UIScrollView that they have been recognized and the UIScrollView will begin scrolling just by the fact that it's not actually visible on screen. Really, this will let us implement exactly what Eliza was trying to do. In order to show how that works, she's going to come back out and do one more demo. Eliza: 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. The first thing I'm going to do is move the scroll view out of the way so that it stops trying to use up all my touches. I can do that by moving at the side by sticking it under everything. Probably, the easiest way to do that is just to hide it, because hidden views don't get hit test. I'm going to go ahead and hide it. 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. 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. Then finally, I'm going to add this dummy view as a sub view of my OpenGL view. That's pretty much it. That's how to solve our problem. Here we are scrolling. I can still scroll first of all which is pretty cool, because the scroll view is actually not even receiving touches now. Then, I can actually now tap these views and it works. This actually does solve the problem. Now, it solves it in an odd way. We have this view here that's performing this weird function and it's a little round about. I want to just mention that under some configurations, you wouldn't actually have to do this. As it happens, my CubeView is detecting touches using touches began moved ended and canceled which makes it vulnerable to this responder chain problem. 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. If you are 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. Had I been detecting taps using a UITapGestureRecognizer, we wouldn't even had this problem in the first place. Keep that in mind 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 and not hide it, not have a dummy view and everything. Turning it back over to Josh for one more topic. Josh: Thanks, Eliza. 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. 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 part way between two cars and land in some intermediate spot. We want to make sure that no matter where they scroll them with their finger, they always land right on a real hole car centered right on our podium. 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, and 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. It may not be initially obvious that that's really why it's there. Let's take a look at it. The delegate method is scroll view will end dragging with velocity, target content offset. It's maybe not entirely clear how this would even do what I just mentioned. The key to it is that last parameter, the target content offset. The reason is because that parameter is actually an in-out parameter. It's a pointer to a CGPoint. On input when this method is called on your delegate, it contains the point of the scroll view plans to decelerate to, but you can change it. If you do, then on a 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 return places that make it shoot of in crazy directions or something, but please don't do that. Just to the small amount and make it land in a reasonable place, but not provide on surprising behavior to your users. 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. By modifying the output parameter there, that's all we have to do. Our scroll view will come to rest at exactly the right spot. Eliza's going to come back and add that to our demo application. Eliza: In this case, let me switch back to the application running. We don't have any cars, but ... Because I couldn't possible draw one in OpenGL. Let's make it so that every time that you scroll this you end up perfectly centered with three cubes like in the middle of the screen, so that this configuration is in the possible resting point for the scroll view just to demonstrate the concept. In order to do that, we're going to add a new method that this CubeView implements. I'm calling it, "Scroll offset for proposed offset." What the CubeView 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. Let's go ahead and implement that. Switching to my implementation, I'm going to add it here under my scrollable content size method. In this scroll offset for proposed offset method, the first thing that I'm going to do is calculate how much have we overshot. Basically, you imagine that here's where you'd be if the three cubes were perfectly centered. In fact, the user has dragged the scroll view over to there. We want to calculate how much did we overshoot the perfect offset. I'm doing that by taking and mod-ding the actual offset X by the width of my little cubes to see how far into a little cube did we get. Now that I've calculated that overshoot, I'm going to see, "Did we overshoot by less than half or more than half of a little cube?" 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. If the overshoot is less than half of a little cube width, then we will make our offset X, we will 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. That's the little cube width minus the overshoot. Then we're just going to return the new offset that we calculated. Let's call that from our view controller. Once again, we're going to add final view controller delegate method, the one that Josh just mentioned. Let me give us more room. In this scroll view will end dragging width velocity target content offset method, we're going to take the target content offset here that you can see at the end. We're going to pass that to our CubeView as the proposed offset. We're going to say, "The new target content offset that we're going to return via this in-out parameter is the results of passing the one we were given to this scroll offset for proposed offset method." That's pretty much it. 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. That's a pretty useful method that I think is cool that got out back in the iOS 5. That's pretty much it for my demos. I'm going to turn it back over to Josh to finish up. Josh: Thanks again for coming. If you have anymore questions about any of the content on the session here, Jake Behrens is the UI Frameworks Evangelist. If you want anymore documentation about scroll view, of course that's the scroll view 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 that you can learn all about it and the repeat at Thursday at 9 AM. Then, "Building advanced gesture recognizers," is also tomorrow, Thursday at 11:30 AM. We will talk more about awesome things you can do at gesture recognizers if you haven't seen a lot about that yet. There's also a scroll view lab tomorrow morning, first thing in the morning 9 AM. If you have more scroll view questions, come see us there. Thanks for coming.

Apple, Inc. AAPL
1 Infinite Loop Cupertino CA 95014 US