Frameworks • iOS • 59:51
UIScrollView is an important building block for constructing iOS interfaces. Join us for a tour of how scroll views are used in new and interesting ways across iOS 7 to create stunning interactions. Learn tips and tricks for using scroll views to create immersive effects in your apps.
Speakers: Eliza Block, Josh Shaffer
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
Good morning. Thanks for coming out for another UIScrollView session this year. For those of you keeping track, this is actually our fifth UIScrollView session in a row. [applause]. So we've talked about a lot of great stuff over the years. And-- So some of you might be thinking, "What could they possibly have left to talk about with ScrollView this year?" But, rest assured, that ScrollViews are an incredibly versatile class and there's a lot of stuff left to discuss that you can do with this stuff.
So, if you're familiar with the structure that viewed in previous years, you probably know that we're going to spend some time going pretty deep into a couple of specific examples of things that you can do with ScrollViews. And we're going to do that again this year and we'll get to that in just a minute.
But before we do, I want to do something a little bit different this year and kind of take a step back and look through iOS 7's new interfaces and see how we're using UIScrollViews in some new ways that you might not necessarily have expected we'd be using UIScrollViews for so you can get some different ideas about how you can use this kind of stuff in your own apps to create some interesting different effects.
So let's get started. The first thing that you see anytime you start on your phone, of course, is the Lock Screen. And starting in iOS 7, the first time that you turn on your phone, every time, the first that you'll do is interact with the UIScrollView. So, kind of big deal.
The Lock Screen is actually now Slide to Unlock is implemented as a two-paged paging ScrollView. So when we swipe over here to the Passcode screen, we're just paging between two pages. It's easy to imagine how that's put together. Over here on the right hand side, we've got the first page and we're just swiping back and forth between them.
Now, even before we get into unlocking, there's more uses of UIScrollView defined right here on the Lock Screen. So of course, if I get a notification saying that I have a WWDC presentation right now in Presidio, that is itself in a vertical scrolling list because you might have multiple notifications to scroll through. Then that's embedded within that outer paging ScrollView. It's an embedded vertical ScrollView. Now there's actually even more because each one of those notifications is itself embedded inside another horizontal paging ScrollView.
So you might wonder why that is and it's actually pretty easy to see once we start interacting with it. When you swipe on an individual notification, that will take you directly to the application that generated that notification. So if we start interacting with this one, you'll find that we can drag it a little bit on its own, but eventually it'll catch the outer ScrollView and start pulling it along with it.
And then if we let go, it just comes back to rest at the beginning. If we were to finish that gesture, we would be scrolling the inner ScrollView for a bit then we would catch the outer one and pull both of them over on to the Passcode screen.
So this is a pretty interesting effect being generated by multiple Nested ScrollViews. And this is actually the first technique that we're going to look at today and go into a lot of depth on how exactly that was implemented. So, we'll show you the construction of the views and see how we make those ScrollViews interact to get that behavior where one pulls the other. But before we get to that, we're going to look through a few more examples in iOS 7 of this kind of thing.
So let's unlock the phone and take a look at some more examples of Nesting ScrollViews. So right here on the home screen, I'm sure as you remember in the past, we used to have spotlight over on the left hand page of our paging ScrollView. In iOS 7, it's moved up to the top and that's implemented as a Nested ScrollView inside the outer paging ScrollView.
Moving into the multitasking user interface, we can now find that the way that we kill applications in iOS 7-- oops. The way that we kill running applications in iOS7 is by taking this interface and scrolling it and flicking it up off the top of the screen. That is also now a Nested UIScrollView. Now, the interesting thing about these Nested ScrollViews is that we get a really consistent experience throughout the interface.
Every different place that we see this kind of interaction feels the same way, has the same bounce, has the same deceleration. It really just makes everything feel natural and consistent across the entire operating system. So if you're looking at doing any of these kinds of interactions, I would encourage you to consider using a UIScrollView to build it because you'll get the same feel that you have across all the other parts of iOS 7.
So Nesting ScrollViews, that's a pretty common one. Another thing that we found has cropped up in a number of different apps on iOS 7 is multiple ScrollViews that scroll through the same content at different rates. So again here in the multitasking UI, you can see that there are actually two independent sections of content. Up at the top, we've got the individual snapshot of the applications and we can scroll through those. And down at the bottom, we have their icons.
Now, as we scroll through the top part, we'll move fairly slowly because we're moving through those images. But you'll notice that at the bottom, the icons kind of group up and now we see more of them at once. What that allows us to do is scroll at the bottom at a faster rate to get through things more quickly. So, two different ScrollViews scrolling through the same content at different rates.
Another place where we find that kind of thing implemented is here in the Calendar application. So we can page through the paging ScrollView at the bottom which represents that days of the week. And as we do that, we go one day at a time. We can see all the days and go nice and slow. If we want to go faster, we can scroll up at the top where those numbers are, and that would let us, with one gesture, move an entire week back with what otherwise had taken us seven swipes to get through.
So this idea of multiple ScrollViews scrolling through the same content at different rates is showing up in a number of different places. It might be something that you might want to consider putting into your own applications if there's appropriate places for that. Another thing I want to take a look at here is some custom content transforms that are being applied to different pieces of content inside UIScrollViews. So, the first one that I'd like to take a look at is here in the Clock application. What you'll find is that there's the 3D Transform being applied to the numbers as we scroll through on the right hand side there.
This has always looked vaguely 3D. But in iOS 7, this is actually a 3D wheel being constructed using 3D Transforms on [inaudible] layers. Now, this is still the exact same scrolling behaviors that you expect across the operating system, the same feel of movement, same deceleration, it's just that there's some transforms applied to the content to give an interesting visual effect that makes it unique in this particular location.
Another example of this kind of thing can be found in Safari. If we go into the new Tab Switcher interface in Safari, you'll find that as you scroll, some of the tabs at the top start to pull out of the screen towards you. Now again, this is just a UIScrollView but we're applying Transforms on the content while we scroll to give this other interesting visual effect that's unique to this application.
One last place where we see this kind of technique being used is in the Passbook application. As we scroll within passes, they're bunching up near the top. And then as we pull down and it rubber bands, they stretch out from one another a little bit. Again, just a UIScrollView, we have the exact same feel of the bouncing, but we get an interesting visual effect unique to the Passbook app. So, all of these techniques are fairly easy to implement by just applying different Transforms to the content while you're scrolling in your UIScrollView.
Now the last thing I want to take a look at before we get into the specific examples of implementing some of this stuff is the Messages application. I'm sure you've noticed as you'd been using Messages that when you go into a conversation and you scroll around it, the Chat Bubbles have a little bit of a bounce to them.
It gives it this really lively feel as you're scrolling around in this application and it's really unique compared to what you see in other apps on iOS 7. Now this is actually the second thing that we're going to talk about today and see exactly how this kind of thing can be implemented. It's actually become really easy to do in iOS 7 with the introduction of the new UIKit Dynamics API. So, we're going to get into that just after we talk through the first Lock Screen example. So, let's get into building some of these things.
And as I mentioned, the first thing we want to talk about is the Lock Screen, those Nested UIScrollViews. So let's take a look at that again just for a quick reminder of how this is going to be put together. So we've got here our Lock Screen. On the left side, we've got another page of content which is the Passcode lock. And of course, this is a paging ScrollView with two pages and our content size is represented by the width of those two pages.
Now, that ScrollView is actually a full screen UIScrollView because it's going to be scrolling all of the content that's visible on the screen. Nested within that, we have the notifications. And that's a much smaller UIScrollView and it scrolls only vertically, but it's here as a child of that outer paging ScrollView. And then as I mentioned, each one of these is also its own nested horizontal ScrollView and they're a little bit smaller because they're just scrolling the individual notification itself.
So we've got one here for the top and then another for the bottom. Now as the user starts to move their finger on this, the content in that will get pulled over to the point where it's going to catch that outer ScrollView. As they finish their gesture and finish scrolling that inner page, it pulls the outer one along with it and gets us over onto the Lock Screen. Now, if you've used UIScrollView before in your apps, I'm sure you're familiar with the fact that ScrollViews will not automatically start dragging their outer containing ScrollView if you just nest one inside another.
So something must've been done in order to make this behavior happen. So let's look at how we would do that. Well, where might we want to implement this kind of thing? If you've watched previous sessions and seen some of the techniques we've used for implementing custom interactions in ScrollViews, you probably already have an idea of what I'm going to say.
And that, of course, is that there's the delegate method, scrollViewDidScroll, which allows us to find out every time a ScrollView's contentOffset changes and do something in response to it. So in this case, the thing that we want to do in response is have the inner ScrollView tell us when it scrolls and then move the outer one along with it. So what's that going to look like? Well, we're going to implement our scrollViewDidScroll delegate method.
We're going to find out how far the inner one has scrolled pass the point where we wanted it to catch the outer one. We want to give some free scrolling before it grabs the outer one, so there's some initial movement that's required. So let's find out how far we got beyond that.
Once we have that value, we're just going to apply it as a new offset onto the outer ScrollView. So we'll take that outer ScrollView and shift its contentOffset by the delta we just calculated. So, actually pretty straightforward. And to show us exactly how we can put that together, Eliza is going to come up and start doing a demo for us.
[ Applause ]
Hi, I'm Eliza and I'm an engineer on the Springboard team. So what I've got here is an application that I built entirely-- well almost entirely using Interface Builder. It doesn't do very much yet, but it illustrates the hierarchy of ScrollViews that Josh already described. So we have an outer paging ScrollView that scrolls between this list of colors and the building. I'm reusing the building views that I wrote for our session two years ago.
So, we can scroll back and forth between those pages. Now, the first page of this two-paged paging ScrollView contains a vertically scrolling collection view. And then each individual cell in the collection view contains a horizontally scrolling ScrollView that scrolls the color for that cell on and off the screen.
So, this is set up with remarkably little code. And what I want to do now is to jump straight to showing you how we can add the behavior that we're getting in the Lock Screen where as you start scrolling one of these cells, it scrolls a little of the way over, catches, and then pulls the outer paging ScrollView with it.
So, of course, this isn't happening by itself. And in order to do this, we're going to need to make a mechanism that the inner ScrollViews can use to communicate with the View Controller that's managing the outer paging ScrollView. And the way that I'm going to do that is a delegate protocol. So here in my scrolling cell, which is a subclass of UICollectionView cell, I'm going to add a scrolling cell delegate protocol that our View Controller is going to conform to so that it can find out when the inner cell has started to scroll.
So, we're going to need a couple of methods in here. We're going to need a ScrollView-- scrolling cell did begin pulling, so that's going to get called when you get your finger passed to that catch point and start pulling the outer guy with you. We need to find out at every frame of scrolling how much the pull offset changed.
So every time that the pull offset changes, the second method will get invoked. And finally, we need to be able to tell our delegate when the scrolling cell stops pulling so that the delegate can clean up any state that it might have put into effect once the scrolling started. OK, so we've got this delegate protocol. We need to make it possible for a delegate to be set on our scrolling cell and then in order to make Xcode happy, we need to pre-declare our scrolling cell delegate protocol.
OK, so now that we've-- I've described what we're going to build, before I go and show you how to implement this scrolling cell side of things, I'm going to head over to the View Controller that is managing the outer paging ScrollView and show how we can actually adopt this delegate protocol because that's actually going to turn out to be very little code. So this is my View Controller class. Pretty much, all it does so far is be a UICollectionView data source and return these scrolling cells.
So, we need to make this View Controller conform to the scrolling cell delegate protocol. When we make a scrolling cell, we need to set its delegate to our self. I'm going to just adjust the number of cells here to make it so we've got more to scroll through. All right, now we need to go ahead and implement these three delegate methods.
What we're going to do is when we find out that one of our little scrolling cells started pulling, we're going to programmatically adjust the contentOffset of the outer paging ScrollView. And because we're going to-- we're basically giving the inner ScrollView control over the outer ScrollView's contentOffset, it turns out to be sort of awkward if we also continue to allow the user to interact with the paging ScrollView while that's happening. So when the scrolling cell begins pulling, what I'm going to do is I'm going to tell the outer ScrollView not to allow user scrolling. And then when the scrolling cell stops pulling, I'm going to re-enable user scrolling on the outer ScrollView.
When the scrolling cell changes its pull offset then, I'm simply going to programmatically set the contentOffset of my outer ScrollView to the correct-- well, to the offset that was reported to me. And then just to make it just a little bit more fun, when we start pulling one of the colors over, let's have the building change its color. So we'll draw a new building with the color of the cell. All right.
So that's pretty much all we need here. So I'm going to switch back to the scrolling cell implementation and we can see how to go about calling these methods to cause the scrolling to take place. So the first thing we need to decide is how far are you going to get in your scrolling of one of these inner ScrollViews before you start pulling the outer ScrollView with you.
And I'm going to do that by simply defining a pull threshold. I've chosen 60, but obviously any number within reason would work. We're also going to need to keep track of some state, are we currently pulling the outer ScrollView because we're going to-- we need to tell the delegate when this starts and stops.
So with that in place, we can then go ahead and implement the UIScrollView delegate protocol. And in particular, we're going to use the scrollViewDidScroll method to find out every time that our inner ScrollView's contentOffset changes. So, we can get the current contentOffset which is, since we only care about the horizontal direction here, it's just the ScrollView's contentOffset.x. Now let's figure out whether this offset represents the beginning of pulling.
So did we just start pulling? We just started pulling if the offset got bigger than the pull threshold and we weren't already pulling. And if that's the case, then we can tell our delegate that the ScrollView-- the scrolling cell began pulling and we can set our flag to Yes. All right.
So if we are pulling, we now need to tell the delegate an additional thing. We need to tell the delegate how much we changed the pull offset. So, we'll calculate what the new pull offset is by subtracting the pull threshold from our current internal offset. And what I've written here is actually not quite right.
So imagine the following scenario. I start scrolling in this inner cell, I get passed to the pull threshold so I'd start pulling, and then I move my finger back and forth a little bit, and I get under and then over and then under the pull threshold. We don't want to report a negative number to our outer paging ScrollView. That would have some weird effect.
So what we want to do instead is make sure that we always report a minimum of zero. So, we're going to take the maximum of zero and the offset that we just calculated. And then we can tell the delegate that the scrolling cell changed the pull offset to that amount. The last thing that we need to do is to figure out when the pulling stops.
And what I've decided to do here is just decide is that the pulling is going to stop when the scrolling stops. So, we need to figure out when the scrolling ends. And if it-- when it ends, we can tell our delegate that the scrolling cell stopped pulling and we can set our flag back to No.
All right, how are we going to figure out that the scrolling ended? If you've seen any of our sessions, I feel like I write this code every year. There's two different ways that scrolling can come to stop. One is that you're scrolling, you have no momentum and you lift your finger, and then the scrolling ends right then and there.
Alternatively, you may have some momentum when you lift your finger, at which point, there's a deceleration period first. So we need to catch both of those cases and we can do that with two further delegate methods, scrolling cell scrollViewDidEndDragging :willDecelerate. And if we're not decelerating, then that means the scrolling ended. Otherwise, we're going to catch the scrolling ended when the deceleration ends. All right, so with all of that in place, I'm going to go head and run this.
We have more cells, which is nice, which we can scroll through. OK, so now if I grab one, it scrolls, it catches, and it starts pulling the outer ScrollView with us which is exactly what we wanted and we can get over to the building. So now, there's a couple of bugs here that I want to draw your attention to if your attention has not already been drawn to them.
One is that we obviously didn't get far enough over, so we're sort of stuck on a weird page that's like in between page boundaries with our building view not all the way in the picture. Another problem is that our cell is missing, the one that we just scrolled over is gone.
And then another problem which is a little bit less obvious, although maybe it was obvious. Here, my finger is where the arrow is and I start dragging and I catch. And now, I would expect that green cell to stay pinned under my finger as it drags the stuff with, but that's not what happens. Instead, it zooms out from under my finger and then disappears. So I'm going to, in a minute, turn things back over to Josh to explain two of these problems.
But the problem where the cells are just gone, that's actually really easy to fix, so I'm going to just fix that right now. That's happening because we're scrolling the content of those cells off the screen when we pull the outer ScrollView over. So we can fix that by simply, you know, our scrolling ended method here just setting the contentOffset of our ScrollView back to zero. But now for the other two bugs, the zooming out from under your finger bug and the fact that we're not getting all the way over to the building page, I'm going to send it back over to Josh.
[ Applause ]
All right, thanks Eliza. So, we're getting pretty close. It's almost there. Just a couple of bugs to take care of, nothing we can't handle. So, let's take a look at how we ended up with only part of that page visible first. Now, we have two pages worth of content. So that gives us a content size represented here by this yellow square, it's about two pages.
And we've defined the size of our inner ScrollView's content to be equal to that. So we've got that nested inner paging ScrollView and it's also got two pages of content. That's how we're going to cause it to get pulled. So let's see what's going wrong as our user starts to drag here.
We start pulling that inner ScrollView a little bit and it moves over to the right. But now you'll notice that the sizes of our content which started out aligned on the left have now pulled away from each other 'cause the inner one has moved a bit but the outer one hasn't started yet.
So now, if we let the user finish the gesture on that inner ScrollView, it's going to finish paging over and come to rest, and it will hit its end before the full content size of the outer ScrollView has been pulled into view. So it's really the case that that inner ScrollView needs to scroll farther than the outer ScrollView because it's going to move a little bit by itself before it catches and starts pulling the outer one with it.
So how do we make the inner ScrollView scroll more? Well, as you know, the way that a UIScrollView's content size is defined is by its bounce-- I'm sorry, of the paging with is defined by its bounce. And in this case, we need two pages of content 'cause we have a two-paged paging ScrollView. So we need a larger bounce in order to get a larger page size so we have more content to scroll.
So, if we look at the bounce of our ScrollView right, it's the exact same width as it its containers. So that gives it the same page size. To get a bigger page size, we have to make it wider. Now, let's find here. We can let it hang off the right side of the screen because it's not drawing anything over there. We're just making it bigger so that it can scroll more content. And then you'll notice that we are still using two pages, so we've doubled our content size and that causes it to hang off on the left as well.
So let's see what happens if we start scrolling with that configuration. Now, we start pulling that inner one in a little bit. And at the point where it's going to catch, the left sides of our content size of both ScrollViews are now aligned. Now, that seems a lot better because if we let our ScrollView finish pulling, it's going to pull that outer one with it, and they'll both come to rest when they have all of their content fully visible. So just making that ScrollView a little wider to give us a wider page and more room to move on the inner ScrollView, fixes that first problem.
But now something else is still wrong because we had that second problem that Eliza showed you where we ended up having the content shooting out from under her finger as she was scrolling the inner ScrollView. So, why was that happening? Well, to understand what was going there, it helps a little bit more to look again at the frame and bounce of the inner ScrollView. So here, we have it visible right in the middle of our screen.
Let's take a look step by step at what's going on again. We start pulling a little bit with our finger. We get to the point where we're going to catch. Now if we hadn't anything else, if we weren't doing the bit where we're pulling the outer ScrollView, what would've just happened is we'd keep moving that inner ScrollView's content along. But of course, what we are doing then is calling setContentOffset on the outer one to cause to move with it. So what we think that should be doing is pulling the ScrollView with it.
Unfortunately, what we forgot about here is that that inner ScrollView is actually a subview of the outer ScrollView. So not only does the outer ScrollView move, but it also ends up pulling the inner one along because it's a child of it. So, we're-- the inner ScrollView is getting scrolled by its own gesture recognizer and then as a result of that, it's telling the outer ScrollView to move which moves it again.
And the really bad part here is that the red area representing our frame is now moving off screen. So, the ScrollView is actually moving out from under our finger. And this is causing the double scrolling Eliza saw which is pulling all the content too fast. So we really want to pull that ScrollView back so the left sides of the content remain aligned and the ScrollView, the inner one, remains centered on the screen.
Now this is actually very similar to a technique that we should a couple of years ago in 2011 for pinning views in place while you're scrolling a ScrollView. This is the same kind of technique that's used in UITableView, when you scroll vertically, you get a table header and it pins to the top of the screen as content scrolls under it.
We want to do the same kind of thing here. We want to pin that inner ScrollView to the left side of the screen so it stays in place even though its parent is moving under it. That way, when the user moves their finger more and lifts, this outer ScrollView will come to restfully on screen and the inner one which is the child of it will still remain in place on screen and everything will work right. So what's that going to look like in our code that we already wrote? Well, it's actually just a really small change to what we've already got, just one little thing that we have to add at the end here.
And that's that we need to translate the child by the same amount that we're pulling the parent. So we want to undo that movement that we're adding. We're going to pull the parent some amount and then translate the child back by the same amount so it doesn't double pull. So Eliza's going to come back now and fix our demo so that this all works.
[ Applause ]
OK. So, we have two problems to fix. The first one, if you recall, was that our page wasn't getting all the way scrolled over, so I'm going to address that one first. We need to go down to the bottom of this file here in a part that I didn't show before which is where we're laying out the subviews of our inner-- internal to the cell ScrollView.
So you'll see that I had calculated a page width to be the size of my cell's bounce that was causing the problem, wasn't wide enough given that we have this pull threshold to contend with. So, what I'm going to simply do is add the pull threshold to the page width. And then because I've actually determined the frame and the content size of the ScrollView in terms of the page width, everything should then just work from that point.
So now to address the issue of the shooting out from under your finger problem and just scroll back up here to my scrollViewDidscroll method. When we tell the delegate that our pull offset changed, the delegate is going to turn around and change the contentOffset of the outer ScrollView which contains us. And so, that's causing us to get pulled along with it resulting in the double scrolling.
So what we'll do to counteract that is to simply set a Transform on the ScrollView which is one of our subviews and we'll set a Transform which simply uses that very same pull offset so that the ScrollView scrolls along with the outer ScrollView and as a result, sort of surprisingly, it actually seems to remain in the same place on the screen. This is a technique that we, as just Josh mentioned, talked about in 2011 with getting a moon to stay fixed on the screen when you're scrolling a bunch of buildings out from under it.
So, when we finished scrolling here and set our contentOffset back to zero, let's also set the Transform back to Identity so that we don't end up in a weird transformed state while we're not doing any scrolling. OK. So with that in place, I'm going to build again. And I can grab one of these cells, it catches and it stays fixed as we want it.
And it gets all the way over to the building this time. So this looks like it's working pretty well. It's doing pretty much what we want. And notice that the buildings change color as planned. So, there's one bug here that may not be immediately obvious and I'm going to do something to make it a little bit more obvious. I'm going to change the pull threshold to a larger number and run it again.
So if I scroll this cell over, now I can get it a little further before it catches and then I let go. Watch really carefully, when I let go so that this going to return back to the zero position, you'll see that the outer paging ScrollView kind of slams into place.
The inner ScrollView comes to a nice decelerated rest as you'd expect from a ScrollView. But the outer ScrollView, it's pretty abrupt that way that it hits the edge of the screen. So, this is actually kind of what you'd expect to happen given the way that this is implemented. When I move this back and forth, you can see that the inner and the outer ScrollViews are moving at exactly the same rate. They're sort of pinned together with 120 points difference.
So when this starts moving back, of course, while the inner ScrollView is still decelerating, the outer ScrollView is going to hit zero and then the inner ScrollView continues. What would be nice would be is if we're in this condition where we're scrolling back to zero, if we could change the rate of scrolling of the outer ScrollView so that both ScrollViews would come to rest at the same time. And I'm going to show you how to do that. It's a technique similar to the techniques that we're using in other places in iOS 7 to get these two ScrollViews moving at different rates.
So the first thing that we need to do to accomplish this is to keep track of whether we're in this decelerating back to zero condition, 'cause we're going to do something special in that case. What we're going to do in that case is move the outer ScrollView at-- with-- at a fraction of the speed of the inner ScrollView. So we need to figure out what that fraction should be.
And I'm going to do that by storing off a deceleration distance ratio. We want the ratio of how far the outer ScrollView has to move compared to how far the inner ScrollView has to move, and then we're going to slow down the scrolling of the outer ScrollView by that proportion.
OK, so how can we detect that we're decelerating back to zero? We can do this by implementing an additional ScrollView delegate method, scrollView WillEndDragging: withVelocity:target ContentOffset. So, you may have seen this method in the past. It's often used to change the landing position of a ScrollView when it starts decelerating.
We're going to use it not for that purpose but simply to find out what the landing position is. Because if the landing position is zero, so if the target contentOffset is zero and our current contentOffset isn't already zero, it means that we're about to start decelerating back to zero which is what we wanted to find out.
So, we'll grab our current offset. And then if the target contentOffset.x is zero but my current offset is greater than zero, that means that I am indeed now decelerating back to zero. And then, of course, when the scrolling ends, I just need to remember to clear that flag. We're not decelerating back to zero once we've stopped scrolling.
OK, so now, if we are decelerating back to zero, we need to figure out the ratio of the distances that are going to be scrolled here. So we already have our own offset. We can calculate the outer offset which is the pull offset, the same way that we did it above. And then our deceleration distance ratio is simply the pull offset divided by the inner ScrollView's offset.
So now we can use that up here in this code here where we were calculating what to report to the delegate. So we were reporting that the delegate's pull offset should always be basically moving together with ours. But instead, we're going to do something different in the case where we're decelerating back to zero. So first of all, if we are not decelerating back to zero, we're going to do the same thing we did before.
But if we are decelerating back to zero, we're instead going to choose a pull offset which is our own offset times that ratio that we calculated. And that's going to cause the outer paging ScrollView to move more slowly when we're in this decelerating back to zero condition. So I'm going to go ahead and run it again. And you can now see that if I let go of this and let it return to zero, they come in for landing together. So that's another technique that's maybe useful. And I'm going to turn it back over to Josh to talk about another topic.
[ Applause ]
OK, so that's topic number one. Now, as I mentioned, we also want to take a look at how we built the Messages Interface with those really interesting bouncy bubbles as you're scrolling through conversations. Now, this is actually a really, really cool stuff. And if you didn't have a chance to watch the earlier UIKit Dynamics talk this week, I would strongly encourage you to go check that out.
There's some very cool stuff there. This is not actually a UI Dynamics session, so we're going to go over a bit of stuff here and give you an idea of how you can use it in this configuration, but we're not going to go into a huge amount of depth on that particular API.
Similarly, we're going to use a UICollectionView in this demo here. And if you haven't used UICollectionView before, there was a great talk last year in 2012 about how UICollectionView can be used, and I'd encourage you to go watch that. We're going to talk about some of the high level details of how you use it here, but we're not going to go into a lot of depth again.
So the really interesting thing that we're going to show is the interesting marriage of those two APIs, CollectionView and UIDynamics, and we'll see how they fit together to make this kind of interface really, really easy to build. So, let's take a look at what it is that we want to build.
Some beautiful great squares. But the interesting thing about them is that when our user scrolls them, we're going to have them get this really nice bouncing effect. I've exaggerated it here so it's more visible than it is in the Messages application. You might want to tone it down a little bit so that you don't, you know, cause your users to go too crazy. But we want to build something that looks just like that.
Now, if we didn't do anything, of course, what would happen as we scroll this kind of content is that everything would just move, stuck together, and it feel pretty static. It's, you know, it's the kind of scrolling you expect to see. But it's not quite as interesting as what we're going for. So what do we have to change in order to make it go from that to the bounciness? We have to have kind of a conceptual shift in what we're trying to do.
So by default, UIScrollViews scroll its content area and all of its children just move along with it directly pinned under the finger, moving with the ScrollView. Instead, we want to behave as if each of these individual cells is pinned to the super view, pinned to that ScrollView at their center point.
And when we scroll, we actually want to move the attachment points instead of the views themselves. We want the individual child elements to remain in place and resist scrolling and just move their attachment points. Then we can attach the individual cells to their attachment points with springs and let the dynamic system pull them along and bounce into place.
So how are we going to do that? Well, you know, you just heard me say, the scrollViewDidScroll delegate method for the last sample, we talked about that a lot in recent years, so you wouldn't be too surprised if I were to say that now. But in this case, we're going to do something a little bit different.
We're going to go ahead and use a UICollectionViewFlowLayout. A UICollectionViewFlowLayout is the object that you use to represent locations of things on the screen within a UICollectionView. So this is the point where if you're not really familiar with UICollectionView, I strongly encourage you to read a little bit more about it. We're going to talk about the details that you need to understand in order to build this UI, but there's a lot more depth here that lets you do even more powerful things if you go a little bit further.
So once we've got our UICollectionViewFlowLayout subclass, we're going to do a few things in it to tie it together with the UIDynamic systems. So we're going to create a UIDynamicAnimator which is the main entry point to UIKit Dynamics. This is the thing that represents the physics world and lets us build up the interface to actually get those bouncing effects.
Now, to create the attachments, the springs that we talked about, we're going to create UIDynamic behaviors, one for each item in our collection view. Now a UIDynamic-- UIAttachmentBehavior is a particular type of behavior that represents a spring, and it's going to give us that bouncing effect that we're looking for.
So then, the only other thing left to do in order to complete that interface will be to actually stretch those springs out as the user scrolls. And we'll do that by stretching them out, obviously. Now, as I mentioned, we're going to create this CollectionViewLayout subclass, so let's see what that looks like.
And now there's three methods that we have to implement. We've got the prepareLayout method, and prepareLayout is called by the collection view to get your CollectionViewLayout subclass ready to display things on screen. So in order to figure out what to display at particular locations, you create these objects called UICollectionView LayoutAttributes. A UICollectionView LayoutAttributes object represents the position of an item in the collection view on the screen.
Now we're going to take advantage of the fact that we're a subclass of UICollectionViewFlowLayout to create these objects for us. Our super class to, the flow layout, already knows how to do layout of the thing-- of things in a particular grid-like structure, so we can just call through the super class to get the initial positions so we don't have to do any of the math to do the initial layout calculations. That's really the benefit of being a subclass of the flow layout here. It's going to save us a lot of code for doing the initial location setup.
Then once we've got that, we're going to create a UIDynamicAnimator which is going to represent that physics world and create all the UIAttachmentBehaviors. We're going to create one UIAttachmentBehavior for each UICollectionView LayoutAttributes object that we have because the layout attributes object represents the element on screen, its position, and the attachment behavior represents the spring attaching it to its attachment point in the ScrollView. So we want one for each paired together.
Now these other two methods that we have here are going to be really, really easy. And so easy that I'm going to write the code on the slide here because it's just that simple. A UIDynamicAnimator and UICollectionViewFlowLayout are designed to work really well together. So in this case, we've got items for Rect as a method on UIDynamicAnimator which is exactly what we need to answer the collection view question, layoutAttributes ForElementInRect, so we can just pass the result directly back.
Additionally, we've got this layoutAttributes ForCellAtIndexPath, and we can return the result of that directly for this other CollectionViewLayout subclass method. So with those two things implemented, that will give us everything we need in order to represent our CollectionViewLayout with the bouncing effects. So then the last bit to represent all of this is stretching the springs.
So how do we do that? Well, as I mentioned, we've got the scrollViewDidScroll delegate method that we usually use for this kind of thing but we're not going to do that this time around. And to understand why, it helps to remember a particular property of UIScrollViews and that is that the ScrollView's contentOffset is equal to its bounds.origin. Those two are the same thing.
Now the reason that that is important in this particular example is because there's actually a method on UICollectionViewLayout that we can take advantage of to get the information we're looking for in the CollectionViewLayout subclass itself. And the reason we want to do that instead of using the delegate method is because we're writing all these codes in a subclass of UICollectionViewFlowLayout, but the flow layout is this layout object that exists to help a collection view display content on screen.
scrollViewDidScroll is a scrollView delegate method. The CollectionViewLayout is almost certainly not the ScrollView delegate. So if we were to try and use that method, we'd have to create a tight coupling between the collection view and the CollectionViewLayout by passing that information about scrolling through and it would create just too much structure when we really want to keep those thing separated.
So instead, we can advantage of this method. It's called shouldInvalidate LayoutForBoundsChange. Now, the nice thing here is that we're passed in the new bounds that we're changing too and since the bounds.origin is the contentOffset, we've got our new contentOffset right there. And at the time that this is called, the bounds has yet to be changed. So if we asked this ScrollView for its current bounds, we'll find out the previous contentOffset. So that let us find out how much has been scrolled since the last time this was called.
So we can do that by just subtracting the y-coordinate of those two values. We're only carrying about vertical scrolling in this case, so that's really easy to do, we just get a CGFloat for the delta. And once we've got that, we can stretch our springs by shifting the positions of each of those layout attributes objects.
The layout attributes represent the current position on screen of the elements and we want them to resist scrolling. So as we scroll up, we want to shift them back down by the delta that we scrolled and let the dynamic system pull them into place and bounce as it goes there.
Then the last bit that we have to remember to do is to tell the UIDynamic system that we've made that change. Now, this is a bit of implementation detail-- not implementation detail. It's an interesting behavior of dynamics that you have to understand to really be able to do this which is that when you create attachment behaviors or really any behaviors in that dynamic system, UIKit Dynamics pull off the values out of your models into the dynamics physics world at the time that you create the attachment behavior.
But we're going to go and update that value by changing it, we just saw on the previous line, by shifting the layout attributes position. If we didn't tell the dynamics system we had done that, it wouldn't get pulled into the physics world, and so the physics system wouldn't have any pulled spring to simulate.
So we have to let this dynamics system know that we've made this change. Now, this is actually going to be done with a method that Eliza is going to show you in a minute that will be available in SID [phonetic] 2, so you'll be able to do this real soon now. To get us started on building this, Eliza is going to come back and do our demo.
[ Applause ]
OK, so let's add this springy behavior to the cells that I've already got in my color collection view. In order to do that, we need to write a subclass of UICollectionViewFlowLayout, so I'm going to add that now, I think. There we go. So we will make a new class, we'll call it "Springy Flow Layout" and it's a subclass of UICollectionViewFlowLayout, add it to my target.
All right, now in my View Controller's [inaudible] file here, I'm going to zoom in a little bit so that you can see. I've got this ScrollView which contains the building view and the collection view and then it also-- and the collection view contains a collection view flow layout. So to adopt this new subclass that I'm about to write, it's going to be simply a matter of changing the class of my CollectionViewFlowLayout to be Springy Flow Layout. It's pretty easy. All right, so let's go ahead and implement Springy Flow Layout.
First thing we're going to need in this flow layout is a-- let's close this, yup. We're going to need a dynamic animator. And we're going to want to create the dynamic animator and fill it with springs. And we're going to do that by overwriting the prepareLayout method of our super class which is UICollectionViewFlowLayout.
And we're taking advantages of the fact that in prepareLayout, our supper class is doing all the math for us. It's figuring out where all of the different cells should go. And it's doing that here in the super implementation of prepareLayout. So now that that's been called, we can go ahead and create our dynamic animator, if we haven't already. And then we can ask the super class for all of the layout attributes that it just computed.
And we'll do that by getting our content size, that's the collection view content size. And then we'll simply ask the super class to give us an array of all of the elements-- the layout attributes for all of the elements in the Rect that is our entire content. Now, this is a convenient method that I'm using here. I've got 80 cells and not that big a collection. If you had a collection of hundreds of thousands of things or even maybe thousands of things, you might not want to load them all into memory at a time.
So note that I am cheating a little bit in this demo. At that point, you might want to tile and I'll show you in a second where you would do that. But-- all right, so for now we've got all of our items and we're going to iterate through them and make springs for each one.
So a spring is UIAttachmentBehavior. We initialize it with the item which is our layout attributes for that particular cell and we attach it to the anchor point which is the item center. And that center was calculated for us by the super implementation of the flow layout. It figured out where that item was supposed to go.
So now that we've got a spring, we need to set its length. Now this is the-- this is important. The spring's length has to be zero because if you think about it, as we scroll, the springs are going to stretch out and then the content is going to bounce around.
If the spring's length was greater than zero, then it wouldn't be guaranteed to come to rest right at its anchor point. It could potentially come to rest at an arbitrary location somewhere near its anchor point. That would give you a collection view with some really weird behavior. When it came to rest, the cells would be kind of overlapping and kind of off center. So we want our spring to be length zero.
We also need to set the dumping and frequency of the spring which I've done by trial and error. These values turned out to be nice. And then we need to tell our dynamic animator about this spring. So we add the spring as a behavior to the dynamic animator. All right, so now we have done this once when the prepareLayout method is first called and that will be good enough. We need to-- in addition, we need to implement these two other methods, overwrite them, rather. So there's layoutAttributes ForElementInRect.
We have to overwrite that method to tell our collection view what items are currently visible in a particular Rect. And notice that the super implementation of this wouldn't work because we're moving the items around using this dynamic system. So we have to ask the dynamic animator which is keeping track of where they really are what's currently available in the Rect and UIDynamics provides this nice call through for that and the same exact logic goes for this layoutAttributes ForItemAtIndexPath method.
All right, so finally, we just need to write the code to stretch the springs when scrolling takes place. And as Josh explained, we're going to do that in shouldInvalidate LayoutForBoundsChange. Let me just get this to be higher up on the screen. OK. So in shouldInvalidate LayoutForBoundsChange, what we're going to do is grab the ScrollView out, which is just our collection view, and find out what its current-- well, how much we just scrolled because remember that this, because the bounds of the ScrollView change every time that its contentOffset changes, we're going to find this method getting called at every frame of scrolling.
So we want to know, what is the delta that we scrolled since the last time this was called? And then we're going to go through all of the springs that we made above and we're going to stretch them by moving the item that that spring owns by the amount that we just scrolled.
So, we grab the item back out from the spring which has an array of items and then we get the center of the item, adjust it by the scroll delta, set it again, and finally call our SUD 2 method, Dynamic Animator Update Item For Current State coming soon. And that will cause the new center to be pulled into the dynamic system and it will cause that bouncy effect to start happening.
Now, the last thing we need to do is tell this method whether indeed it should invalidate its layout. We're going to say No because the dynamic animator is going to be moving the items around and that itself will invalidate the layout, so we don't need to double invalidate it.
We can say, "No, it doesn't have to invalidate 'cause of the bounce change." Instead, it will be invalidated in a second because of the moving of the item. All right, so let's see what that looks like. All right, so I'm going to grab this cell, purple cell here, and I'm going to start scrolling.
Now, you'll notice we have a bouncy effect. It's not really the bouncy effect we wanted. First of all, my finger, as presented by this giant mouse pointer, is coming off the cell that I grabbed. See how it gets away ahead and then when the thing comes to rest, it's back under the finger again? This is really not the behavior that we wanted. There's two problems. First of all, the cell right under my finger should track my finger. Otherwise, you have this weird effect where you're not directly manipulating the content. And second of all, all of the cells are bouncing together. They're not coming apart as we wanted.
So if you look back at the code, this is actually not surprising. We're adjusting the center of every single item in our collection view by the same amount at every frame. So of course, they don't come apart. And that's also why that content is coming out from under my finger. All of the cells are resisting scrolling by the same amount. So I'm going to turn it back over to Josh one more time to explain the technique for actually fixing this and getting the behavior that we want.
[ Applause ]
OK, so we left off on last time that we're looking at our video of this stuff with this exact look here, but we didn't actually play it. If we had let go at this point and allowed the dynamic system to take over and run that spring simulation, we would've seen exactly what Eliza saw of everything bouncing together.
Now, of course, if we did that a little bit more, you'd see we stretched the springs and they all move the same amount and everything bounces together. It's exactly what we saw happen in Eliza's demo and clearly not what we were trying to build. So what change do we have to make in order to fix this? Well, we still want to move all of the individual attachment points together the same way that we thought we were doing.
The thing that we want to do differently is that we want to stretch the springs at different amount. And we want to stretch them by an amount that varies based on how far they are from the user's finger on the screen. The ones that are directly under the finger should stay under the finger and track directly.
So we don't want to stretch those springs really at all because we don't want that to be springy, we want it to follow. And the farther away the cell gets from the finger, the more we want to stretch out those springs. So that's going to look something more like this.
The user puts their finger down on the screen and starts scrolling, the cell directly under it has stayed directly under it, they've bunched up in the direction that we're scrolling towards, and spread out in the direction that we've scrolled away from. And if you look at our green lines here representing the spring lengths, you can tell that the ones that are farther away from the finger had been stretched more than the ones that are right underneath.
If we let that go, then they're individually going to bounce at different speeds. Each one's going to have a slightly different feel and it'll get that kind of behavior that we were looking for. So if we do that, we see different amounts of stretching varying by how far it is from the finger, and we get that really nice bouncing feel we were after. So Eliza is going to modify our demo now to get that effect.
[ Applause ]
All right, so we have a ScrollView and that turns out to be really convenient. We want to figure out where did the user put their finger down so that we can stretch the springs that are farther from the finger more than the springs that are right under the finger.
So, we'll use the fact that ScrollViews expose their pan gesture recognizer and pan gesture recognizers expose the location of the touch in whatever view you want. So we'll simply say the touch location is the ScrollView's pan gesture recognizer's location in the ScrollView. So now we know where the user has the touchdown and we can take advantage of that to figure out how far that touch location is from each individual spring. So, we'll grab the anchor point of the spring that represents that particular cell's resting position. And the distance from the touch is just the difference between the y-coordinate of the touch location and the anchor point.
Now we want to scale the amount that we resist the scrolling by that distance. So I'm going to make a variable here, scroll resistance, and it's just going to be a fraction of the distance from the touch. Now this is something you can play around with. Basically, the more scroll resistance, the bouncier. So if I have a lot of scroll resistance on a particular cell, then it's going to-- the spring is going to stretch more and it's going to bounce more. And so you can play with different denominators here. This one gets us a fair amount of bounciness.
So now, when we adjust the center of the item, instead of just adding the scroll delta, what we want to do is add the scroll delta times that scroll resistance fraction that we calculated. So when we're at a cell that's right under the user's finger, the distance from the touch is going to be pretty close to zero, so the scroll resistance-- rather, the amount that we're changing the center will be very small. And when the touch is very far from the finger, we'll be changing the center by more.
So now there's one sort of-- this isn't quite right as I've written it because we never want to change the center of this item by more than the scroll delta. We always want to be basically adjusting it by something in between zero and the scroll delta because if we changed it by more than the scroll delta, then instead of just resisting scrolling, it would actually be moving in the opposite direction from scrolling, and that would be very strange. So I'm going to, in fact, have this at one.
So we want the minimum of the scroll delta and then the adjusted scroll delta by the scroll resistance. All right, so with that in place, we can go ahead and build this. Now, if I grab this cell here and I scroll, it stays under my finger, which is good. And-- oops.
And the content bounces around exactly as we hoped. The stuff in the direction of scrolling gets closer together and the stuff farther from the scrolling gets farther apart. So that was the effect that we wanted. Before I turn it back over to Josh, because we have another minute or so, let me just show you where you would do this tiling that I mentioned. You don't want to load all of these springs into memory at the same time, most likely, in real use case.
So here where I'm adding behaviors to the spring, what you could basically do is add only the behaviors that are in near what's visible on the screen at a given time. And if you want to look at-- look back at some of our previous sessions, we talked a lot about tiling. So this is a slightly weird use of tiling where instead of tiling views, you're tiling springs, just this-- you're only making the springs that you need in order to control the content that's pretty close to what's on screen.
And you can do that by-- over here in the UIDynamicAnimator header, you can see that you can both add and remove behaviors from the dynamic animator. So if you wanted to do this in a slightly more memory-conscious way, that would be the place to look. All right, so back over to Josh.
[ Applause ]
So not to point out bugs, but some of you probably noticed that when scrolling down, there wasn't quite the same bouncy effect that we were hoping for there. That is a result of us changing the demo late last night, and apparently not verifying that we had done it correctly.
There's actually-- I have made that exact same mistake myself at my desk when looking at this demo in the past. There's actually an issue where if you do the max in that particular way, you actually pick the value that's always greater than zero and sometimes we want to go negative. So max isn't always what we want. Sometimes we actually want to go min. We could go back and fix that, but maybe live debugging it on stage isn't the best plan right now. [Inaudible Remark] You can go and find our bug later.
- It would've helped if I built in run.
- So over the past few years, we've had quite a few of these sessions, as I mentioned earlier. Since 2009, there's been a UIScrollView session and I wanted to give you a real quick reference of the different kinds of topics we'd covered in that time. So, I don't expect that you're going to read all these right here right now, but we've covered a lot of stuff. We've got photo browsing, tiling, infinite scrolling.
We did a really interesting thing with OpenGL scrolling last year so that you could figure out how to use UIScrollViews within your OpenGL games and applications. I'd strongly encourage you, if you're doing anything with UIScrollView, to come back and look through this list and see if there's anything that applies to the things that you're trying to do and go and, you know, go back and watch some of these sessions in the WWDC app that Jake has, you know, kindly given us access to this year.
If you have more questions about this stuff, Jake Behrens is the UI Frameworks Evangelist-- or sorry, the App Frameworks Evangelist now. Documentation, of course, we've got the UIScrollView Programming Guide and the Apple Developer Forums are a great place to find out about this stuff. You know, I'm on there a lot and a lot of other folks are as well. There are some related sessions.
If you missed it, there's the Building User Interfaces with iOS 7 one that was on Tuesday, and Getting Started with Dynamics was also on Tuesday. Later today, there's an Advanced Techniques with UIKit Dynamics actually right here at 3:15. So thanks very much for coming and please enjoy the rest of WWDC. [Applause]
[ Silence ]