Learn how to use UICollectionView, with highly reusable UIKit components and some MVVM pattern without the going nuts with index path calculations.
TL;DR: download the example project from gitlab.
You might have noticed that I have a love for metal music. In this tutorial we're going to build an Apple Music catalog like look from ground zero using only the mighty UICollectionView class. Headers, horizontal and vertical scrolling, circular images, so basically almost everything that you'll ever need to build great user interfaces. 🤘🏻
Basics of UICollectionView
If you're not familiar with collection views, I'd suggest to learn them immediately. They're the basic building blocks for many apps provided by Apple and other third party developers. It's like table views on steroids. Here is a quick intro about how to work with them through IB and Swift code. 💻
How to make UICollectionView with interface builder?
This is the very-very-very basics. If you look for a detailed tutorial, please read the official docs by Apple, or you can scroll down a little bit and skip this brief intro.
Anyway, here are the main steps to create your first UICollectionView based UI:
- Drag a UICollectionView object to your view controller
- Set proper constraints on the collection view
- Set dataSource & delegate of the collection view
- Prototype your cell layout inside the controller
- Add constraints to your views inside the cell
- Set prototype cell class & reuse identifier
- Do a little coding
In a nuthsell, the data source will provide all the required data about how to populate the collection view, and the delegate will handle user events, such as tapping on a cell. You should have a clear understanding about the data source and delegate methods, so feel free to play with them for a little while. ⌨️
How to setup UICollectionView programmatically?
So, let's make a playground, because playgrounds are fun to work with and they hang almost every 2nd time of running your code on the mac. 🔨
As you might have noticed cells are the core components of a collection view. They are derived from reusable views, this means that if you have a list of 1000 elements, there won't be a thousand cells created for every element, but only a few that fills the size of the screen and when you scroll down the list these items are going to be reused to display your elements. This is only because of memory considerations, so unlike UIScrollView the UICollectionView (and UITableView) class is a really smart and efficent one, but this is also the reason why you have to prepare (reset the contents of) the cell every time before you display your actual data. 😉
Initialization is also handled by the system, but it's worth to mention that if you are working with Interface Builder, you should do your customization inside the awakeFromNib method, but if you are using code, init(frame:) is your place.
Next we have to implement the view controller which is responsible for managing the collection view, we're not using IB so we have to create it manually by using layout constraints - like for the textLabel in the cell - inside the loadView method. After the view hierarchy is ready to rock, we also set the data source and delegate plus register our cell class for further reuse. Note that this is done automatically by the system if you are using IB, but if you prefer code you have to do it by calling the proper registration method. You can register both nibs and classes.
This time you should pay some attention on the flow layout delegate methods. You can use these methods to provide metrics for the layout system. The flow layout will display all the cells based on these numbers and sizes. sizeForItemAt is responsible for the cell size, minimumInteritemSpacingForSectionAt is the horizontal padding, minimumLineSpacingForSectionAt is the vertical padding, and insetForSectionAt is for the margin of the collection view section.
Using supplementary elements (section headers and footers)
So in this section I'm going to both use storyboards, nibs and some Swift code. This is my usual approach for a few reasons. Altought I love making constraints from code, most people prefer visual editors, so all the cells are created inside nibs. Why nibs? Becuase if you have multiple collection views this is "almost" the only nice way to share cells between them.
You can create section footers exactly the same way as you do headers, so that's why this time I'm only going to focus on headers, because literally you only have to change one word in order to use footers. ⚽️
You just have to create two xib files, one for the cell and one for the header. Please note that you could use the exact same collection view cell to display content in the section header, but this is a demo so let's just go with two distinct items. You don't even have to set the reuse identifier from IB, because we have to register our reusable views inside the source code, so just set the cell class and connect your outlets.
Cell and supplementary element registration is slightly different for nibs.
Implementing the data source for the section header looks like this.
Providing the size for the flow layout delegate is also pretty straightforward, however sometimes I don't really get the naming conventions by Apple. Once you have to switch a kind, and the other time there are exact methods for specific kinds. 🤷♂️
Starting from iOS9 section headers and footers can be pinned to the top or bottom of the visible bounds of the collection view.
That's it, now you know how to build basic layouts with collection view, but what about complex cases, like using multiple kinds of cells in the same collection view?
Things can get pretty messy with index paths, so that's why I re-invented something better based on a technique how to build advanced user interfaces with collection views showcased by Apple back at WWDC 2014.
Let's build a CVVM framework
Ok, now you know the basics, so why don't we build a framework? Let's start by inventing a brand new arhitecture on top of MVVM principles but specially for UICollectionViews. I simply call it the CVVM™ pattern. 😅
I'll explain the components real quick and after that you'll learn how to use them to build up the Apple music-ish layout that I was talking about in the beginning. 🎶
The first problem with collection views is the size calculation. You have to provide the size (width & height) for each cell inside your collection view.
- if everything has a fixed size inside your collection view, you can just set the size properties on the flow layout itself
- if you need dynamic sizes per item, you can implement the flow layout delegate aka. UICollectionViewDelegateFlowLayout (why is the delegate word in the middle of the name???) and return the exact sizes for the layout system
- if you need even more control you can create a new layout subclass derived from CollectionView(Flow)Layout and do all the size calculations there
Thats good, but still you have to mess with index paths, trait collections, frames and many more in order to have a simple 2, 4, n column layout that adapts on every device. This is the reason why I've created a really basic grid system for size calculation. With my grid class you can just set the number of columns and get back the size for x amount of columns, "just like" in web based css grid systems. 🕸
The snippet above defines a 6 column grid system and returns a size object that has a width of 4 items plus 3 times the paddings and a height of 120 pixel, based on the frame for a given view with some margins in mind. 💡
Registering and reusing cells shoud and can be automated in a type safe manner!!! You just want to use the cell, and you shouldn't care about reuse identifiers and cell registration at all. I've made a couple helper methods in order to make the progress more pleasant. Reuse identifiers are derived from the name of the cell classes, so you dont't have to worry about anymore. This is a practice that most of the developers use.
view model = cell (view) + data (model)
Filling up "template" cell with real data should be the task of a view model. This is where MVVM comes into play. I've made a generic base view model class, that you should subclass. With the help of a protocol, you can use various cells in a single collection view without going crazy of the row & section calculations and you can focus on one simple task: connecting view with models. 😛
section = header + footer + cells
I'm trying to emphasize that you don't want to mess with index paths, you just want to put your data together and that's it. In the past I've struggled more than enough with "unnecessary index path math", so I've made the section object as a simple container to wrap headers, footers and all the items inside of the section. The result? Generic data source class that can be used with multiple cells without any row or section index calculations. 👏👏👏
So in order to make all the things I've mentioned above work, I needed to implement the collection view delegate, data source, and flow layout delegate methods. That's how my source class was born. Everything is implemented here, and I'm using sections, view models the grid system to build up collection views. But hey, enough from this theory, let's see it in practice. 👓
A basic list of albums
How to make a simple 2 column list hassle free.
Step 1. Make the cell.
This step is identical with the regular setup, except that your cell have to be a subclass of my
CollectionViewCell class. So I'd suggest to make your xib file, add your class and do everything as you would do usually.
Step 2. Make the view model.
Now instead of configuring the cell inside the delegate, or in a configure method somewhere, let's make a real view model for the cell & the data model that's going to be represented via the view.
Step 3. Setup your data source.
Now, use your real data and populate your collection view using the view models.
Step 4. 🍺🤘🏻🎸
Congratulations you're done with your collection view. With just a few lines of code you have a ROCK SOLID code that will help you out in most of the situations! 😎
This is just a the of the iceberg! 🚢
Horizontal scrolling inside vertical scrolling
What if we make a cell that contains a collection view and we use the same method like above? A collection view containing a collectionview... UICollectionViewception!!! 😂
It's completely possible, and really easy to do, the data that feeds the view model will be a collection view source object, and you're done. Effortless, magical and super nice to implement, also included in the example app.
Sections with artists & circular images
Multiple sections? No problem, circular images? That's also a piece of cake, if you had read my previous tutorial about cirular collection view cells, you'll know how to do it, but please check out the source code from gitlab and see it for youself in action.
Callbacks and actions
User events can be handled very easy, becuse view models can have delegates or callback blocks, it only depends on you which one you prefer. I've made examples for both cases, don't hesitate to see for yourself. 🤓
Dynamic cell sizing reimagined
I also had a tutorial about collection view self sizing cell support, but to be honest I'm not a big fan of Apple's official method. After I've made the grid system and started using view models, it was more easy to calculate cell heights by myself, with about 2 lines of extra code. I believe that's worth it, because self sizing cells are a little buggy if it comes to autorotation.
Rotation support, adaptivity
Don't worry about that too much, you can simply change the grid or check trait collections inside the view model if you want. I'd say everything (except moving cells, and a few more things, but it's WIP) is supported right out of the box, this method is just a lightweight wrapper around the official collection view API. That's the beauty of it, because it's lightweight you can do with it whatever you want or use it in a way that you prefer. 📦
What if I told you...
If you like this method that's cool, but what if I told you that there is more? Do you want to use the same pattern everywher? I mean on iOS, tvOS, macOS and even watchOS. Done deal! I've created everything inside the CoreKit framework. UITableViews, WKInterfaceTables are supported as well.
I really believe that Apple this year will approach the next generation UIKit / AppKit / UXKit frameworks (written in Swift of course) somewhat like this. I'm not talking about the view model pattern, but about the same API on every platform thinking. Anyway, who knows this for sue, we'll see... #wwdc18 🤔
Now go, grab the sample code and listen to some metal! 🤘🏻