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.

4.8 (28) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

  1. updater is an object conforming to ListUpdatingDelegate, which handles row and section updates. ListAdapterUpdater is a default implementation that’s suitable for your usage.
  2. viewController is a UIViewController that houses the adapter. IGListKit uses this view controller later for navigating to other view controllers.
  3. workingRangeSize is the size of the working range, which allows you to prepare content for sections just outside of the visible frame.
Note: Working ranges are a more advanced topic not covered by this tutorial. However there’s plenty of documentation and even an example app in the IGListKit repo!

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
  }
}
Note: IGListKit makes heavy use of required protocol methods. Even though you might end up with empty methods, or ones that return 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 provide loader.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 plain ListSectionController 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.

Note: Objects can change multiple times during the lifetime of a section controller. This only happens when you start unlocking more advanced features of IGListKit, like custom model diffing. You won’t have to worry about diffing in this tutorial.

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:

  1. If the index is the first, use a JournalEntryDateCell cell, otherwise use a JournalEntryCell cell. Journal entries always appear with a date followed by the text.
  2. Dequeue the cell from the reuse pool using the cell class, a section controller (self) and the index.
  3. Depending on the cell type, configure it using the JournalEntry you set earlier in didUpdate(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:

  1. The collectionContext is a weak variable and must be nullable. Though it should never be nil, it’s best to take precautions and Swift guard makes that simple.
  2. 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.
  3. 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:

IGListKit