IGListKit Tutorial: Better UICollectionViews
In this IGListKit tutorial, you’ll learn to build better, more dynamic UICollectionViews with Instagram’s data-driven framework. By Ron Kliffer.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
IGListKit Tutorial: Better UICollectionViews
30 mins
ListAdapter and Data Source
With UICollectionView
, you need some sort of data source that adopts UICollectionViewDataSource
. Its job is to return section and row counts as well as individual cells.
In IGListKit, you use a ListAdapter
to control the collection view. You still need a data source that conforms to the protocol ListAdapterDataSource
, but instead of returning counts and cells, you provide arrays and section controllers (more on this later).
For starters, in FeedViewController.swift add the following at the top of FeedViewController
:
lazy var adapter: ListAdapter = {
return ListAdapter(
updater: ListAdapterUpdater(),
viewController: self,
workingRangeSize: 0)
}()
This creates an initialized variable for the ListAdapter
. The initializer requires three parameters:
-
updater
is an object conforming toListUpdatingDelegate
, which handles row and section updates.ListAdapterUpdater
is a default implementation that’s suitable for your usage. -
viewController
is aUIViewController
that houses the adapter. IGListKit uses this view controller later for navigating to other view controllers. -
workingRangeSize
is the size of the working range, which allows you to prepare content for sections just outside of the visible frame.
Add the following to the bottom of viewDidLoad()
:
adapter.collectionView = collectionView
adapter.dataSource = self
This connects the collectionView
to the adapter
. It also sets self
as the dataSource
for the adapter — resulting in a compiler error, because you haven’t conformed to ListAdapterDataSource
yet.
Fix this by extending FeedViewController
to adopt ListAdapterDataSource
. Add the following to the bottom of the file:
// MARK: - ListAdapterDataSource
extension FeedViewController: ListAdapterDataSource {
// 1
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return loader.entries
}
// 2
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any)
-> ListSectionController {
return ListSectionController()
}
// 3
func emptyView(for listAdapter: ListAdapter) -> UIView? {
return nil
}
}
nil
, you don’t have to worry about silently missing methods or fighting a dynamic runtime. It makes using IGListKit very hard to mess up.FeedViewController
now conforms to ListAdapterDataSource
and implements its three required methods:
-
objects(for:)
returns an array of data objects that should show up in the collection view. You provideloader.entries
here as it contains the journal entries. - For each data object,
listAdapter(_:sectionControllerFor:)
must return a new instance of a section controller. For now you’re returning a plainListSectionController
to appease the compiler. In a moment, you’ll modify this to return a custom journal section controller. -
emptyView(for:)
returns a view to display when the list is empty. NASA is in a bit of a time crunch, so they didn’t budget for this feature.
Creating Your First Section Controller
A section controller is an abstraction that, given a data object, configures and controls cells in a section of a collection view. This concept is similar to a view-model that exists to configure a view: the data object is the view-model and the cells are the view. The section controller acts as the glue between the two.
In IGListKit, you create a new section controller for different types of data and behavior. JPL engineers already built a JournalEntry
model, so you need to create a section controller that can handle it.
Right-click on the SectionControllers group and select New File. Create a new Cocoa Touch Class named JournalSectionController that subclasses ListSectionController.
Xcode doesn’t automatically import third-party frameworks, so in JournalSectionController.swift, add a line at the top:
import IGListKit
Add the following properties to the top of JournalSectionController
:
var entry: JournalEntry!
let solFormatter = SolFormatter()
JournalEntry
is a model class that you’ll use when implementing the data source. The SolFormatter
class provides methods for converting dates to Sol format. You’ll need both shortly.
Also inside JournalSectionController
, override init()
by adding the following:
override init() {
super.init()
inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
}
Without this, the cells between sections will butt up next to each other. This adds 15 point padding to the bottom of JournalSectionController
objects.
Your section controller needs to override four methods from ListSectionController
to provide the actual data for the adapter to work with.
Add the following extension to the bottom of the file:
// MARK: - Data Provider
extension JournalSectionController {
override func numberOfItems() -> Int {
return 2
}
override func sizeForItem(at index: Int) -> CGSize {
return .zero
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
return UICollectionViewCell()
}
override func didUpdate(to object: Any) {
}
}
All methods are stub implementations except for numberOfItems()
, which simply returns 2 for a date and text pair. If you refer back to ClassicFeedViewController.swift, you’ll notice that you also return 2 items per section in collectionView(_:numberOfItemsInSection:)
. This is basically the same thing!
In didUpdate(to:)
, add the following:
entry = object as? JournalEntry
IGListKit calls didUpdate(to:)
to hand an object to the section controller. Note this method is always called before any of the cell protocol methods. Here, you save the passed object in entry
.
Now that you have some data, you can start configuring your cells. Replace the placeholder implementation of cellForItem(at:)
with the following:
// 1
let cellClass: AnyClass = index == 0 ? JournalEntryDateCell.self : JournalEntryCell.self
// 2
let cell = collectionContext!.dequeueReusableCell(of: cellClass, for: self, at: index)
// 3
if let cell = cell as? JournalEntryDateCell {
cell.label.text = "SOL \(solFormatter.sols(fromDate: entry.date))"
} else if let cell = cell as? JournalEntryCell {
cell.label.text = entry.text
}
return cell
IGListKit calls cellForItem(at:)
when it requires a cell at a given index in the section. Here’s how the code works:
- If the index is the first, use a
JournalEntryDateCell
cell, otherwise use aJournalEntryCell
cell. Journal entries always appear with a date followed by the text. - Dequeue the cell from the reuse pool using the cell class, a section controller (
self
) and the index. - Depending on the cell type, configure it using the
JournalEntry
you set earlier indidUpdate(to object:)
.
Next, replace the placeholder implementation of sizeForItem(at:)
with the following:
// 1
guard
let context = collectionContext,
let entry = entry
else {
return .zero
}
// 2
let width = context.containerSize.width
// 3
if index == 0 {
return CGSize(width: width, height: 30)
} else {
return JournalEntryCell.cellSize(width: width, text: entry.text)
}
How this code works:
- The
collectionContext
is aweak
variable and must be nullable. Though it should never benil
, it’s best to take precautions and Swiftguard
makes that simple. -
ListCollectionContext
is a context object with information about the adapter, collection view and view controller that’s using the section controller. Here you get the width of the container. - If the first index (a date cell), return a size as wide as the container and 30 points tall. Otherwise, use the cell helper method to calculate the dynamic text size of the cell.
This pattern of dequeuing a cell of different types, configuring and returning sizes should all feel familiar if you’ve ever worked with UICollectionView
before. Again, you can refer back to ClassicFeedViewController
and see that a lot of this code is almost exactly the same.
Now you have a section controller that receives a JournalEntry
object and returns and sizes two cells. It’s time to bring it all together.
Back in FeedViewController.swift, replace the contents of listAdapter(_:sectionControllerFor:)
with the following:
return JournalSectionController()
Whenever IGListKit calls this method, it returns your new journal section controller.
Build and run the app. You should see a list of journal entries: