Using UICollectionView to build lists on iOS 14
Ever since UICollectionView
was first introduced in iOS 6, it has occasionally been tricky to decide whether to use a collection view or a table view to build a given list-based UI.
Up until this point, however, UITableView
did have some quite substantial advantages over its collection view sibling in terms of list-specific features — since table views come with things like easy reordering, deletion and swipe actions, as well as system-provided styling — all built-in. But this year, UICollectionView
has gained a remarkable number of new features when it comes to building lists, and is now arguably the better option in the vast majority of situations.
Let’s take a first look at some of those new APIs, and how we can get started using them by building a simple collection view-powered list.
Setting things up
Let’s say that we’re working on a view controller for rendering a list of contacts, which has a view model that contains its data, as well as a UICollectionView
and a UICollectionViewDataSource
for rendering that data. Within our viewDidLoad
method, we then connect our data source to our collection view (both of which are lazily created in order to avoid non-optional optionals) and tell our view controller to update its list — like this:
class ContactListViewController: UIViewController {
private let viewModel = ContactListViewModel()
private lazy var collectionView = makeCollectionView()
private lazy var dataSource = makeDataSource()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = dataSource
view.addSubview(collectionView)
updateList()
}
...
}
Next, let’s go ahead and implement all of the methods that we call above, starting with makeCollectionView
.
Configurations and registrations
Last year, at WWDC 2019, Apple introduced the new compositional layout API for collection views, which — like the name implies — enables various kinds of layouts to be composed from smaller building blocks.
New this year is that such layouts can now be easily created (with two lines of code!) for common list variants, such as plain
, grouped
and insetGrouped
— which in turn enables us to quickly configure a list-like collection view like this:
private extension ContactListViewController {
func makeCollectionView() -> UICollectionView {
let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let layout = UICollectionViewCompositionalLayout.list(using: config)
return UICollectionView(frame: .zero, collectionViewLayout: layout)
}
}
Another thing that’s new this year is the CellRegistration
API, and cells can now also be configured in a much more declarative way using content configurations.
Cell registrations provide a new, much more type-safe way to tell UIKit what UICollectionViewCell
subclass to use to render a given cell, and using a cell’s defaultContentConfiguration
, we can bind our data to it without having to be aware of its exact subviews.
Here’s how we could combine those two new APIs to create a CellRegistration
that uses the new UICollectionViewListCell
to render our contact list’s various cells:
private extension ContactListViewController {
func makeCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewListCell, Contact> {
UICollectionView.CellRegistration { cell, indexPath, contact in
// Configuring each cell's content:
var config = cell.defaultContentConfiguration()
config.text = contact.name
config.secondaryText = contact.email
cell.contentConfiguration = config
// Configuring a trailing swipe action for deleting a contact:
cell.trailingSwipeActionsConfiguration = UISwipeActionsConfiguration(
actions: [UIContextualAction(
style: .destructive,
title: "Delete",
handler: { [weak self] _, _, completion in
self?.deleteContact(withID: contact.id)
self?.updateList()
completion(true)
}
)]
)
// Showing a disclosure indicator as the cell's accessory:
cell.accessories = [.disclosureIndicator()]
}
}
}
Note how we pass the given contact’s id
into our deleteContact
method when our swipe action was triggered, rather than using its IndexPath
. Apple recommends never using index paths as identifiers, as they’re not guaranteed to be stable as list items get inserted and removed.
Apart from increased type safety, a major benefit of the above new API is that we can perform all of our cell configuration, as well as our swipe action event handling, within a single closure. That in turn means that we have to keep track of fewer pieces of state ourselves, which should lead to an overall simpler implementation in most cases.
Constructing and updating a diffable data source
Now that we’ve created our collection view and have all of our cell configuration code in place, let’s now construct our list’s data source. To do that, we’ll use the built-in diffable data source API that was introduced last year, in combination with the CellRegistration
instance that we built above, like this:
private extension ContactListViewController {
func makeDataSource() -> UICollectionViewDiffableDataSource<Section, Contact> {
let cellRegistration = makeCellRegistration()
return UICollectionViewDiffableDataSource<Section, Contact>(
collectionView: collectionView,
cellProvider: { view, indexPath, item in
view.dequeueConfiguredReusableCell(
using: cellRegistration,
for: indexPath,
item: item
)
}
)
}
}
Finally, it’s time to populate the above data source with our contact list data. To do that, we’ll first create a Section
enum to describe our list’s two sections, and we’ll then insert the data from our viewModel
into those two sections using the NSDiffableDataSourceSnapshot
API:
enum Section: CaseIterable {
case favorites
case all
}
private extension ContactListViewController {
func updateList() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Contact>()
snapshot.appendSections(Section.allCases)
snapshot.appendItems(viewModel.favorites, toSection: .favorites)
snapshot.appendItems(viewModel.all, toSection: .all)
dataSource.apply(snapshot)
}
}
With the above in place, we’ve now built a simple grouped contact list containing two sections, with support for swipe actions and disclosure indicators — all using UICollectionView
.
Conclusion
The cool thing is that not only is UICollectionView
now more or less on par with UITableView
in terms of list rendering — it has even surpassed it, since the new outlines that are now heavily used within sidebars on iPadOS are only available through collection views.
So while there still might be edge cases that only UITableView
covers, I think it’s fair to say that UICollectionView
is now the preferred way of building lists on iOS (at least once we’re able to use iOS 14 as an app’s minimum deployment target).
For more information, I recommend checking out the following WWDC session videos:
- Advances in UICollectionView
- Advances in diffable data sources
- Lists in UICollectionView
- Build for iPad
- Modern cell configuration
Thanks for reading! 🚀
Fast and rock-solid Continuous Integration. Automatically build, test and distribute your app on every Pull Request — which is perfect for teams that are now working remotely, as you’ll quickly get feedback on each change that you make. Try out their new, improved free tier to get started.