Frameworks • iOS • 54:53
Building an advanced user interface with collection view requires a great design, careful code architecture, and often times a custom layout. Learn how the iTunes team used UICollectionView to deliver a new version of the iTunes Connect app with an updated user interface incorporating pinning headers, swipe to edit and reorder, and a manageable code-base.
Speaker: Jeff Watkins
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
So good afternoon, everybody. My name is Jeff Watkins. I'm an iOS Software Engineer. I'm on the iTunes team and I have the pleasure of talking to you a little bit about Advanced User Interfaces with Collection View. Recently, we got some brand new designs for iTunes Connect. And these were welcome new designs for iOS 7 look and feel and they were really fantastic.
They were brand new, fresh, clean look and feel and they gave us a great opportunity because we have been making decisions for our codes since iOS 2. And as you can imagine, the decisions that we made back in iOS 2 were really not the same decisions that we would be making today.
So, this was an ideal opportunity for us to take some time and pay off some technical debt. Now, we all know that that's fancy speak for "throw away the old code and write some shiny new code." But the reality is, we really wanted to build a really great modern architecture that would take us forward a few more releases. You know, everybody thinks that they're going to build this shiny, glittering jewel of an architecture that's going to last forever. But the reality is you get two, maybe three releases out of anything you build and then it's time to rethink things, but this was our opportunity.
So, I'm really excited to announce that we have some sample code that goes along with this talk. But what makes this sample code even more interesting is this sample code is distilled down from the actual source code of iTunes Connect. I have my manager and his chain of management to thank for this because this is kind of unusual.
Normally sample code is something that you would just sort of whip together for your talk and it covers the aspects that you're talking about. But it, you know, it covers the bare minimum. This actually is full, rich data sources, full, rich UICollectionViewLayout and there's actually way more in there that we're going to talk about today.
[ Applause ]
Plus, I'm sure there are bugs. But more importantly, we'll be building on it and improving it as time goes on. So, look for all sorts of additions to this sample code as time goes on. So, we had some requirements just like you all probably get requirements with your applications and I'd like to go through those one by one and take a look at them and give you a little bit of a sense of where we were coming from and, you know, a little bit sense of the terror that I felt when I took a look at them. So first of all, we had really complex data.
We started off with iTunes Connect 2 supporting apps and books, and that was great. But now, we were going to support all of the content types that the store supports. So we were going to add music, we're going to add music, as well as movies and TV seasons.
Now, that was going to post a significant challenge for us, because each one of these content types as you can see has multiple sections, multiple different tabs within there, we're talking about an awful lot of data that's going to be a real difficult time to manage. On top of that, we really wanted to have a single loading indicator.
One of the things that I think is really tremendously important is that users know when the content is available and ready for interaction. I really don't like it when an application has a spinner here, spinner there, and I don't really know if it's ready for me. So I wanted to make iTunes Connect really clear that when the spinner was gone, we were ready for you. So, we were going to have our spinner where our spinner is going to go away and there was the content.
Now, most of our designs looked like UITableView and this was easy. We could have done this in our sleep. But, it got more interesting on iPad because we needed to support multiple columns. Now, we could have hacked together a solution for iPad but that didn't feel right, right? Here we're trying to create a future-looking modern architecture where you shouldn't consider hacks.
So, we really needed to do something more sophisticated for iPad and that's when I started thinking, "Let's take a look at collection view." Well, if you know collection view, you know there are some limitations with the standard flow layout. Specifically, it doesn't really support global headers like you have in table view, right? And we needed to support global headers. We needed to be able to have this one header that stuck around as you tabbed through your content.
So that made me start thinking, "I'm going to have to do something here and I might wind up having to write my own layout." In addition, we also needed to support pinned headers. So, as we're scrolling through our content, it is really important that we able-we'd be able to get at those segmented controls so that we could get to the different sections of our content.
So we needed to be able to support pinned headers and flow layout also doesn't really support that either. So, that kind of put the nail in the coffin of using the flow layout for us. And that meant we're going to have to do a collection view layout which I got to admit, it was pretty exciting.
I also wanted to add swipe to edit. This was a new feature in iOS 7 and I thought it would be a great addition to iTunes Connect because it gives it that modern interactive feel and users can then swipe, delete Xcode from their list of favorites and move on.
[ Applause ]
Also, you know, if you've got a list of favorites, you're going to want to be able to edit those, so we wanted to support batch editing, and likewise, you can delete Xcode. Now, favorites wouldn't be any good, right, if you couldn't manipulate them, if you couldn't reorder your favorites because it'd be kind of a drag if you got a lot of favorites and they're in the wrong order.
So we wanted to support drag reordering as well and we needed to support that with a custom layout and that was going to be a bit of a challenge but I feel like I've been slagging on Xcode so I'm going to put it back at the top of the list of favorites because they really have hit it out of the park. Come on.
[ Applause ]
With Swift and all of the other stuff they've been doing, those guys-so, those were our requirements and that was kind of a lot, I mean on top of all the other stuff that the rest of the guys had to do. But I want to talk about the first one first because without data, right, iTunes Connect is nothing.
They would just be some pretty pictures of your apps and everything else which are nice but nobody is going to want to look at, you know, lines and pretty pictures. So, getting the data right is the most important thing and quite frankly, it was the thing that I was really, really convolutedly freaked out about because five data types and all these different sections and, oh my goodness. So, I really needed to come up with some way to minimize the complexity of all that data.
And the way I took a look at this is I took all of the designs that our HI guys and gals, in fact, mostly a gal had come up with, and we lay them out and I kind of squinted them, add them and lay them out side by side and I realized that there were some real similarities here, right? If you squinted this, you realized that we've got some key values stuff going on here and that in particular showed up all throughout the application.
We've also got this Status section that shows up frequently and we've got lots of sections in our layouts that seem to reappear all over the place. And I thought to myself "Gosh. This is really something that we need to take advantage of." We really need to be able to reuse this kind of code.
But the problem is, if you've been doing collection view controllers in the past, you know that there's really challenges with code reuse when it comes to data sources. The traditional approach to a collection of your data source has you putting everything in the view controller. And as a result, you'll wind up with one of the two things. First, you can wind up with a gigantic view controller. And I'm sure you've all seen this-not on your projects.
But you wind up with the view controller that's like 5,000 lines of code. And it does everything including things that you don't do anymore. I deleted all that code. And so, we didn't want that especially now, I mean we're starting fresh, right, we wouldn't want that. The other option is we could have had one view controller for each content type and that seems like it's a better approach but it's really not because there's really a lot of things that the view controller just does innately and the way you wind up with sharing common code is you wind up pushing it down to a common base class, which winds up being the same thing as having one gigantic view controller because you've pushed it all to the base class. So sure you've kind of hidden it a little bit but now it's down there and you're just not looking at it. And it doesn't at all solve the problem of code reuse across screens.
So we wanted a better solution. Now fortunately, I had the pleasure of working on the Game Center team during the iOS 7 redesign and they came up with a solution to this exact problem. It's called Aggregate Data Sources. Now their data was a little bit less complex than ours by just a smidge.
But they hit upon the right answer, which was building up data sources from smaller data sources. There's nothing that says that a UICollectionViewDataSource has to be implemented on your view controller. In fact, I would encourage you not to implement the data source on your view controller. In fact, I'd go so far as to say, never ever again implement it on your view controller.
[ Applause ]
I mean, unless you really want to, but what we do here is we implement it as a general NSObject, and then we build those together to build a much more sophisticated data source and this goes a long way to enabling code reuse. Because as you can imagine, we've got these little classes of data sources that we cobble together and we reuse them all over the place. And as a result, we wound up with a single view controller for our product detail screen that has only 14 methods.
Now, six of those methods-before you get all upset that I have 14 methods, six of those methods, five of them are building the five content types of data sources, one of them is building the overall data source. And then the rest of them are some action methods that bubble up our responder chain.
I think there's some, you know, delegate methods in there and whatnot. So I mean, we do go a little overboard. I probably could have cut it down but, you know, we're getting there. So let's take a look at the four intrinsic aggregate data source classes that you'll see in a sample code.
The first and most important one where all of the action is is AAPLDataSource. That's the base data source. That's where we implement the UICollectionViewDataSource protocol, as well as a host of other good stuff that we'll talk about in a little bit. On top of that, we've layered the AAPLSegmentedDataSource and that can have multiple children but only one of them is active at a time. Think of a UISegmentedControl, in fact, there's a good reason to think of that. It will vend out a header with a UISegmentedControl as part of its base behavior.
Then there's the AAPLComposedDataSource and that will take a number of children. They're all active at once and it manages the mapping between the external IndexPaths and the internal child IndexPaths. And then there's this AAPLBasicDataSource. How many times have you had just a list of things and you just want to show them? Well, that's what the basic data source is for.
It takes an array of items. It manages insertions, deletions, reorderings, all that nasty stuff. It sends out the right notifications. It only allows you to have one section because there's countless times where that's all you need and we wind up using it all over the place. So let's take a look in how we use these four classes to build up our product details data source.
So first of all, the product details data source is a segmented data source and it has four segments. No surprise, one for Details, one for Episodes, one for Reviews and one for Trends. Those correspond exactly to the UISegmentedControl in the headers. That UISegmentedControl is actually created by the Segmented Data Source.
Now, the Details child data source is a composed data source. And it has children for the Status section, Information and Description. Each one of those is its own data source. The Information was one of those key value data sources. The Description is a special textual data source. And all of these are pulling information out of the product object.
Now, Episodes is just one of those basic data sources because we've just got a list of Episodes. But this is where it gets interesting because as you can see, Episodes have a show date. Now we could take the date and we could pass it off to the cell and we could have the cell create an NSDateFormatter and we could render and do that, but we've been told countless times that's really the wrong thing to do, right? So this is what data source has allow us to do is we can encapsulate task-specific logic, presentation-specific logic.
So, in my Episode's data source, that's where I have a date formatter that's specific to the Episodes and I do that conversion of the NSDate, that's in each episode, into a string before I jam it into the cell. And that way, I get the best performance rather than allowing that to happen in each cell.
Then Reviews is another composed data source. You're probably getting tired of this but it's great. Let me tell you. And the composed data source for Reviews has one for Ratings and another one for the Actual Reviews. And we'll actually come back to the Reviews data source when we look at how things load in the upcoming slides. And then Trends is a custom data source. It derives directly from the base data source. Because we go out and we fetch the trend data and then we actually render it in two separate sections. One for the Graph and then another for the Historical Data.
So that's how we build up a product details data source from all these little aggregate data sources. And that has an additional benefit. Because remember I told that we wanted to have a single loading indicator, right? Well, we originally tried to make our single point of truth for whether we're loading via the view controller.
And that's kind of tough because the view controller really doesn't know what all is being loaded, right? We've got five different content types, multiple sections with subsections within them. Each one loads its own content, how is it going to know? Well it turns out, we have one thing that knows about everything and that's the product details data source.
So the answer to the whole problem of who knows what's loading, is the data source. The data source is responsible for loading its content. And when you think about it, it actually makes total sense. And if you make the data sources responsible, they know just the data they need to load.
They know exactly how to load it and they're already responsible for their own task-specific logic so they can format it, do whatever they need to and make it ready for presentation. So now, the view controller kicks off the whole process in -viewWillAppear by sending a load data to the data source which in turn propagates that message to its children as appropriate.
So for example, a segmented data source will only send it to the selected data source, but a composed data source will send it all of its children. And because we're good computer scientists just like all of you, we use a state machine to keep track of everything. You would, right? Let's take a look at that state machine.
[ Applause ]
So the obviously named AAPLLoadableContentStateMachine, it's got a few states. It's not as nasty as it could be. All data sources start off in the initial state until they receive a load content message. Once they've received the load content message, then they transition into the loading content state. That's when we display the spinner. That's the only time we ever display the spinner. And if you noticed, you can't ever get back there.
So when they get content or they get an error or they receive no content from their respective sources, they'll transition into no content, content loaded or an error state and will display the appropriate view. And we'll take a look at exactly how that works. So let's see how this all works from a data source loading data off the network and from a UI standpoint. So here we are in the initial state. And we've got my Cat List Data Source. This is from the sample application because I am cat crazy. And yes, I was heartbroken when we switched to California landmarks.
Yeah, I thought the Ocelot was finally going to get its chance. So were starting off in the initial state and we get the load content message and we transition into loading content. Then the data source, in this case, it's going to request out to its server, "Get me some cats." The server eventually comes back and says, "Here you go." Now depending on what the response is, then it's going to make a transition into the right place. And let's take a look at what the UI does in this case.
So here we are in the loading content, we're showing the spinner and let's imagine we get back some cats. So here we're going to display the cats. Like I said, this is the sample application. Assuming everything works well, this will compile and run exactly as planned on your machines. And we'll see our list of big cats right there on our devices.
However, if you know anything about cats, they're never where you expect them to be. So more than likely, you're going to try to load the cats and you're going to be told, there are no cats here. Well, the great thing about this is I had to do no work here, which is great because cats do a lot of work for you.
So in this case, all I set up was the No Content message and the No Content title on my data source. And behind the scenes, the machinery took care of everything else. When I transition into the No Content state, the layout and the data sources take care of presenting the place holder for me.
It really takes a lot of burden off of my shoulders. Now, sometimes, things go wrong. And then, we go from the Loading Content state into the Error state. And similarly to No Content, if we've configured an Error message and an Error title, we'll get a placeholder telling the user what's gone wrong.
So this is a really great way that we found to get our consistent UI but it's a little bit more than because in most cases, we're not just loading one thing, right? I mentioned, we'd come back to the Ratings and Reviews. We load that information separately. We fetch the ratings and we fetch the reviews.
Well, we can't update if we've got a single loading indicator. We can't update the Collection View with the Ratings and then the Reviews because that wouldn't look right. So we've got to update everything all at once. And in order to do that, we needed a solution that was elegant because anything less would be just wrong.
So the solution that we came up with relies on the fact that we have a parent-child relationship to our data sources. So let's take a look at that. All of our data sources start off in the initial state. They get the load content message and then it all transitioned into the loading content state at which point, the ratings and the reviews data sources send out their request to the server.
Now, we all know that the servers not going to respond simultaneously to both requests. That's fantasy land. So what happens is one of the responses comes back first. That data source will process the response but not update itself. What it does is it queues up a block that actually will do the update and it sends it up to the parent chain and that block will just sort of hang out there for a bit.
Then, the next data source will get its response, it does the same thing. It transitions into Content Loaded, queues up an update block. And then the parent will discover, "Oh, look. All of my children are loaded. It's OK for me to transition into Content Loaded." And now I can call performBatchUpdates on the collectionView and schedule all of those update blocks safely inside of performBatchUpdates block.
[ Applause ]
The good thing about all these is we don't get exceptions because of our timing inconsistencies. And I don't know about you, but I don't like exceptions. My boss gets really grouchy. So to recap, Aggregate Data Sources were a great way for us to reduce our view controller complexity. Our view controller only does view controllery things.
It no longer involved in the data source other than setting it up. And it went a long way to promoting code reuse. So now our code is scattered in these aggregate data sources that we use all over the place and it isolates task-specific logic that we use for setting up ourselves into the data sources where they're appropriate. And we got that single loading indicator that we were looking for.
So that was our first two requirements. So let's take a look at the next four, which necessitated a Custom UICollectionViewLayout. Now, I have a confession to make. The first time I built the layout, I was very unhappy. It worked. It worked actually really well. But I am-how shall I put this, hard to please? And it didn't work as well as I wanted.
And the reason is I didn't collect all the information that I should have. I tried to be done and just move on to other things. So the message I want you to take away from all of this-and I'm sort of skipping ahead to the summary before I even start-is do your bookkeeping.
Get all the information you can. And at the very end, run instruments to make sure that you have enough memory and you're not using too much resources and then prune back the information you're keeping but keep it all upfront. So let's take a look at the information I kept, the information I didn't keep and what I should have kept.
So first, what did I need to keep and where did it belong? So obviously, we got these great designs from HI and they're just pictures, right? We need to interpret that and figure out what did we need from that to actually layout cells and headers and footers and supplementary views and decoration views and the whole nine yards. And then, kind of as a footnote, where does it all go? And I want to address that first, get it out of the way.
Data sources vend visual information. They vend views. Design metrics are visual information. I put them in the data source. Partly because the data sources are hierarchal and they've got default metrics and they've got section-based metrics, it just made sense to put it there. I could have put it in a parallel structure but then I would have parallel structures and that would have been too crazy. So, they're in the data sources. That's where you'll find them. So let's take a look at the section metrics. So here, we've got some Big Cats and naturally we're going to want to know the rowHeight.
We also want to know the backgroundColor, right? A lot of our sections can be gray. Some of them are white. None of them are garish colors. Obviously, we have some that have separators and separatorInsets but not all of our sections have separators. So we needed to be able to set that on a per section bases. We also needed to be able to set a selectedBackgroundColor.
The way we determine whether or not a cell appears selectable is by the selectedBackgroundColor. Whether it actually is selectable is in code. Now remember, we also support multiple columns so we needed to know, based on the section, how many columns it should support and whether or not it should show a column separator.
For the headers and footers, we similarly needed to know the height, although most of the time we set this to zero which means figure it out yourself, thanks to auto layout. We also wanted to know the backgroundColor but most of the time we set this to nil which means inherited from the section.
And we also want to specify a padding because one of the things that we saw in our designs a lot was exactly the same header but with a little bit more space between this instance of the header and that instance of the header. And yeah, we could define a subclass to give us a little extra space or we could just have another metric that says, "Oh, here we're going to have 10 points of spacing, there we're going to have 20." So we added padding.
It actually wound up being hugely helpful when we needed to align things horizontally as well. So this gave us all the information we needed to lay things out in our actual layout. But some of this information needed to be passed along to the cells and the headers and, to do that, we had some Custom Attributes. And I'm sure you all know that you can create subclasses of the layout attributes so that's exactly what we did, and we wound up with four plus a few more Custom Attributes-- first of all, backgroundColor and the selectedBackgroundColor, and padding but also pinnedHeader.
At one point, we thought that we wanted our headers to respond to being pinned. For example, if we'd had a navigation bar that was blue, it would make perfect sense for the header to reach the top pin and change to be blue, and it would need to know that it was pinned in order to do that.
So those were the custom attributes we decided to define for headers and footers and our cells. Now remember, we also needed to support Global Headers. And to do that, I'm going to let you in on what I thought was a secret. It turns out it's not. We're all familiar with NSIndexPath normally having two indices, one for Section, one for Item. Well, it seems that you can also create an NSIndexPath with one index and when you do that, you're creating a Global Supplementary View indexPath or a Global Decoration View indexPath.
It makes no sense two create these four items so don't try. But this is how we separated our global headers from everything else. When we initially tried this, I tried to lump them in with section zero and it just made no sense. So now, I'm able to distinguish them from everything else and we treat global headers differently. When you look at the sample code, you will notice that the global headers actually never go off screen.
They pin just underneath the navigation bar. They're treated as what we'll call special attributes. And they get updated as you scroll so that they stay in place and they do all sorts of funny, fancy things. Now, the code needs to be a little bit smart so that you check the length of your index path, so that you don't accidentally call section and item for these global index paths. But other than that, you're good.
Now, building a layout. Earlier today, you might have heard Olivier mention that you want to be really careful about using invalidation or you want to be really careful to use invalidation, I should say. And that's exactly the case. You might be tempted to build the most perfect, single-pass layout engine. Don't, because what you'll wind up with is absolute efficiency the first time. And then someone will come in and-the collection will come in and say, "Hey, this cell has changed." And you'll have to re-layout everything.
Or the origin has changed because you've scrolled and you'll have to lay out everything and you'll just drop. Your performance will die. So let's take a look at the pseudo-code so to speak, for the layout that we came up with. And then, we're going to show you a little bit of how we snapshot the metrics for one of our sections. And I'll think you'll get a good sense of why breaking this up into sections is really important.
So first of all, if the data source is changed significantly, like it's a totally different one or the total number of sections have changed, we just throw everything away. And the reason for this is it's probably more efficient for us to just re-compute everything than it would be to compute the deltas because our data sources are actually pretty small.
If you've seen iTunes Connect, you know that the actual content isn't that big. So, yeah, I could probably come up with a clever way to compute deltas and everything else but this wound up being just easier. And in the long run, getting it done is sometimes just as important.
Next, if the collection view's width has changed or obviously if I threw everything away, then I need to regenerate all the layout attributes as well as collect all of the special layout attributes. So all of those global headers and any pinned headers that might have been defined within the content. So, I make that single pass and I layout everything.
And then, if the origin has changed because we've scrolled, I update the position of any special layout attributes. Now when I go back and I update all of this code to support iOS 8, I'm going to fix this because I'll use the new invalidation context and this all will be different but this is how we do it today. So let's take a look at how we snapshot the metrics.
So we have this hierarchal structure of data sources. And on your right, is the product detail screen. So we start with Section 0, which is the Status, and we begin our snapshot at the very least specific data source, which is the Segmented Data Source. Now in this case, these are actually the default metrics for the entire product details. And that's rowHeight equals 44. BackgroundColor equals light-grey.
That means everything is rowHeight equals 44, backgroundColor equals light-grey. So that's what we start with. Next, we move to the next most specific data source which is the composed data source for details. It just so happens that it doesn't define any metrics whatsoever. Not default metrics, not section-based metrics.
So then we can move on to the status data source which has section-based metrics specifically for that section of rowHeight equals 60 and selectedBackgroundColor of mediumGrey. Because remember, we do the selection based on the actual color. So, we have these overrides for rowHeight and we have a new attribute for the selectedBackgroundColor.
So you can imagine, if we had to go through this, every time we move the origin of the collection view, we'd get terrible frame rates. So, it's really important that we only do this when it's absolutely necessary. So we only do it when the collection view changes. But then, we go through and we update our attributes when we actually scroll.
So I spoke earlier about Optional Layout Methods and this is where my whole confession comes in. I did these out of order. I know I spoke to some of you in the lab earlier and I gave you some specific details. I started with -initialLayoutAttributes and -finalLayoutAttributes and I realize that, "Oh, my goodness. In order to do -initialLayoutAttributes and -finalLayoutAttributes, I need the update information." So let me implement -prepareForCollection ViewUpdates and I'll just grab the array of updates. I'll just stash those over here because I need them.
And each time I would get called for -initalLayoutAttributes, I'd walk the array and I try to figure out what was going on and quickly my head exploded because it was just too hard. And I admit, I deleted all the code. I went on to other things and it was weeks before I was willing to come back to it. And fortunately, in those weeks, I talked to a lot of people. I read a lot of the documentation and I finally figured out what I was doing wrong. And what I was doing wrong is I wasn't doing my bookkeeping. I wasn't collecting the information that I needed.
And so let's talk about that information and more importantly let's talk about the information in the right order. So this it turns out is the right order. Prepare for layout. You probably already implement this if you're doing a layout but there's some information that we need to capture here, not just building our layout. And then -prepareForCollection ViewUpdates followed by -targetContentOffsetFor ProposedContentOffset, which is quite a mouthful, and then -initialLayoutAttributes and -finalLayoutAttributes. So I'm going to talk about -prepareLayout and -prepareForCollection ViewUpdates together because they do sort of pair up.
First, -prepareLayout. So here's my layout, very colorful. And when I get -prepareLayout, what I do is I take a snapshot of the current layout and I keep it as my old layout. In the process of building my layout, I have a lookup table between all the IndexPaths to items, all the IndexPaths to supplementary views, all the IndexPaths to decoration views.
Remember I said, keep everything. Well, when I get called for -prepareLayout, I duplicate everything. I stash it as the old layout. Then, in -prepareForCollection ViewUpdates, I create a lookup table for deletions, insertions and everything that was reloaded. And I run through the array of updates and all the deletions get tracked and all the insertions also get tracked.
So now, I know everything that's going on in my layout. And then before I'm done with -prepareForCollection ViewUpdates, I calculate the delta in the height of the two layouts as well as the change in where the offset of the start of my content should be. Because I've got content that's pinned and possibly scrolled off the screen.
It gets a little bit tricky. And for the sake of the examples, we're going to ignore all that. But that all gets done right here before I return from prepareForCollection ViewUpdates. So, let's take a look at one of the other methods I didn't implement and later wished I had, the -targetContentOffset method.
Now, the documentation says that this adjusts the scroll offset and I didn't understand viscerally, how important this was until I saw it in action. And so I have animations that I'm going to show you just how important this is. It's used by my layout actually to calculate the pinning offset and the delta that I use to prevent unwanted motion. And the trick is I get this calculation correct, which took a little while, but then I used it in so many places.
So let's take a look at that exactly how that works. Before I implemented this, here I've got my old content and it's 320 by 1000 and I'm scrolled all the way to the bottom. And I put in some new content which is considerably shorter and collection view, remember, does a lot of work for us and it says, "Hey, that's not valid. I'll help you out.
I'll adjust your contentOffset to 0, 165. I'll animate everything down for you." Well remember, I'm not easy to please and I don't want it there. I want to be able to see the new content beginning at the new content. That's kind of why I put it there. So, that means to me that I need to change my contentOffset.
So, after implementing target contentOffset for proposed contentOffset, here I've got my old content again with the original contentOffset of 0, 432. And this time, I propose-give a new contentOffset of 0, 0 and notice, whoosh, everything slides down and I can see my content at the correct position. But I'm still not happy because my content shouldn't be moving. If you've seen the sample app or you've seen iTunes Connect or if you've seen Game Center, you actually know that I wanted my content not just to come in. I wanted my content to slide in from the side because frankly that's cool.
And if I've got content that slides in from the top and slides in from the side, my users are going to get seasick. I don't want that. I'm going to get one-star reviews for that. So the solution is -initial and -finalLayoutAttributes. That function that I tried to do first is the final one that I needed to implement. And just a reminder, initialLayoutAttributes is called if the view will be on the screen after the update. finalLayoutAttributes is called if the view was on the screen before the update and they're both going to be called for a view that remains on the screen.
And I know you're not going to believe this until you see the code, but it's actually really simple once all the information is been processed. So, let's look at some more animations before I let you see the code. On the left here, we see what's happening to the viewport. On the right is what the user actually sees, OK? Without initialLayoutAttributes, the blue content is stuck where it really is and the viewport slides up to meet it and that makes that unwanted animation.
However, after I've implemented initialLayoutAttributes, you'll notice that the new content is synced up with the viewport and as the viewport transitions to its new location, the new content animates with it and appears to stand still. Now, everything is moving but nothing appears like it's moving, and that's what's important.
So, let's take a look at the actual code. I promise you it's actually really simple. So, this is the first half. We start off by getting the section then we get a copy of the layout attributes, and obviously we need to copy because we're going to modify them possibly and we don't want to modify the real attributes because then we'll be modifying the real attributes and then our cells will be horrible.
So then, we just determine whether or not this particular item was inserted or reloaded. Remember I said we did all this calculation in -prepareForCollection ViewUpdates, and now that means we've only got these two lines of code. Then it was inserted. We changed the alpha to be 0 so it's going to fade in.
If it was reloaded and it didn't exist in the old layout, remember I created the snapshot of the old layout. So, if there is no item at that IndexPath in the old layout when it was reloaded, we want it to fade in. It's perfectly OK. If there was an item there the previous time then it's just going to change. I'm OK with that. And then finally, we want to offset the origin of the item by the delta of the content offset. And that way, everything is going to stay exactly in place.
So, to recap, bookkeeping is critical to making your layout work. If you don't have the information you need, there's no way you can make these methods work. And then the optional methods really make a huge difference. They're the difference between a layout that technically works and a layout that's just great.
So, middle four requirements. I think we did a pretty good job with that. The last three I thought were just going to be, you know, a little something extra that we'd add to the application. And they changed from being a little something extra when they started to take a little bit of time so let's take a look at them.
In order to add Swipe to Edit, obviously we needed to add actions to our cells. And there's any numbers of ways we could have added actions. But in this case, we add them directly to the cells. And here we are because we're going to use this as an example. We're adding two actions to a cell, makeFavorite and swipeToDeleteCell.
You can see that each action has a title and a selector. Those selectors when they get-- when the actions invoked, bubble up the responder chain and our view controller catches them. Now, for swipe to edit and batch editing and the whole nine yards, we needed some additional custom attributes.
We needed the columnIndex, the editing state, and whether or not a cell was movable. And I think those will become apparent why we needed them in just a moment. So, for Swipe to Edit with one column, this works exactly as you would expect, right? Swipe over, you get the two buttons, you tap on a button, actually goes up the responder chain, view controller catches it. Hooray, all is good.
Now, in the case of the delete action, that's all handled by the base view controller. For two columns, it gets a little bit more complicated because, just like Game Center, we wanted it to appear that our cell was sliding underneath the other content. And in order to do that, we needed to have the column index as an attribute on the cell. So when the column index is not 0, we display a little gradient and therefore everything looks like it's sliding underneath.
So for batch editing, we have an attribute for whether it's editing. And when that changes to No, we animate out our editing controls and if movable is Yes, then we'll also display the gripper. And movable is actually determined based on a query to your data source or our data source really. And as you would expect, you tap on the twisty thing and out come the editing controls.
And because I am much addicted to state machines, it is all controlled by another state machine. This manages all of our gesture recognizers, the UIPanGestureRecognizer, the UILongPressGestureRecognizer. The states are somewhat complex. The diagram gives you an indication. The reason for that is that we have to respond to external stimulus.
So for example, if you have a toolbar that has an Edit and a Done button that toggles. If you're editing and the user taps Done, at any given moment, we have to shut it all down and transition back into the idle state. And so, that means that things are a little bit more complex. I'd love to revisit this and see if I can't, you know, simplify it, but it works and that's, you know, a key criteria.
It only works with our layout in our cells. I tried at some length to generalize it a little bit more, but it works and that's a great feature. So, Drag to Reorder. It required some layout changes. In order to make everything work correctly, we need to add a layout gap. So as you're dragging the cell around, we needed to split apart the existing cells. And we also needed to take the cell that you were dragging and mark it as Hidden.
And we also needed to make sure that our layout calculation was fast enough. I'm not going to pretend that it's as fast as it's going to get because I'm not done yet, but it was fast enough. It feels reasonably fluid. It's good enough for the first release. Now, it does require data source support. So by default, the data source normally answers, "Nope, you can't move that one. Nope, you can't edit that," so on and so forth.
So, in order to actually implement drag reordering in your code, you're going to want to say, "Oh yes, you can move those thing. You can drag those things." And so there are some methods on your data source that need to have the right responses. But it really was an incremental change to our layout and an incremental change to our data sources.
So, that's the last three requirements. And they were a little bit of a challenge but they were just, you know, a little bit extra. So in summary, the aggregate data sources went a long way to simplifying our complex designs. When I first saw the designs that HI gave us, I was a little bit terrified. There was a lot there. We had a very short time and I'm glad that we found a way to make it easier.
And for UICollectionView? Bookkeeping, bookkeeping, bookkeeping. Keep all the data. Don't let it out of your sight. At the very end, when you're concerned that you're using too much memory, fire up Instruments. Let Instruments tell you if you're using too much memory. And then finally, Swipe to Edit, Drag Reordering-they're just incremental things. You can do it. And they're sample code.
Pull it out of there and make it part of your own. So, for more information, talk to the incomparable Jake Behrens. I'm told he has great shoes. Documentation, take a look at the iOS documentation online and the Dev Forums. There's great material there. We've been known to hang out there as well. There were related sessions this morning. I'm sure they'll be online this evening. So, thank you so much for coming and I hope you enjoyed this evening.
[ Applause ]