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
Each app starts off the same way: a few screens, some buttons and maybe a list or two. But as time goes on and the app grows, features start to creep their way in. Your clean data sources start to crumble under the pressure of deadlines and product managers. After a while, you’re left with the massive view controller ruins to maintain. Lucky for you, there’s a solution to that problem!
Instagram created IGListKit to make feature creep and massive view controllers a thing of the past when working with UICollectionView
. By creating lists with IGListKit, you can build apps with decoupled components, blazing-fast updates and support for any type of data.
In this tutorial you will refactor a basic UICollectionView
to use IGListKit, then extend the app and take it out of this world!
Getting Started
You are one of NASA’s top software engineers and on staff for the latest manned mission to Mars. The team already built the first version of the Marslink app.
Use the Download Materials button at the top or bottom of this tutorial to download it. After you’ve downloaded the project, open Marslink.xcworkspace, then build and run the app.
So far, the app just shows a list of astronaut journal entries.
You’re tasked with adding new features to this app whenever the crew needs them. Familiarize yourself with the project by opening ClassicFeedViewController.swift and having a look around.
If you’ve ever worked with UICollectionView
, what you see looks pretty standard:
-
ClassicFeedViewController
is aUIViewController
subclass that implementsUICollectionViewDataSource
in an extension. -
viewDidLoad()
creates aUICollectionView
, registers cells, sets the data source and adds it to the view hierarchy. - The
loader.entries
array powers the number of sections, each having just two cells (one for the date and one for the text). - Date cells contain the Sol date and text entry cells with
Journal
text. -
collectionView(_:layout:sizeForItemAt:)
returns a fixed size for the date cell and calculates the size of the text for the actual entry.
Everything seems to be working just fine, but the mission director comes up with some urgent product update requests:
An astronaut has just become stranded on Mars. We need you to add a weather module and real-time chat. You have 48 hours.
Engineers from JPL have some of these systems working, but they need your help adding them to the app.
If all the pressure of bringing an astronaut home wasn’t enough, NASA’s head designer just handed you requirements that each subsystem’s update in the app has to be animated, which means no reloadData()
.
How in the world are you supposed to integrate these new modules into an existing app and make all the transitions animated? The astronaut only has so many potatoes!
Introducing IGListKit
While UICollectionView
is an incredibly powerful tool, with great power comes great responsibility. Keeping your data source and the view in sync is of utmost importance, but disconnects here commonly cause crashes.
IGListKit is a data-driven UICollectionView
framework built by the team at Instagram. With this framework, you provide an array of objects to display in UICollectionView
. For each type of object, an adapter creates something called a section controller, which has all of the details for creating cells.
IGListKit automatically diffs your objects and performs animated batch updates on the UICollectionView
for whatever changed. This way you never have to write batch updates yourself, avoiding the issues listed under caveats here.
Adding IGListKit to a UICollectionView
IGListKit does all the hard work of identifying changes in a collection and updating the appropriate rows with animation. It is also structured to easily handle multiple sections with different data and UI. With that in mind, it’s a perfect solution to the new batch of requirements—so it’s time to start implementing it!
With Marslink.xcworkspace still open, right-click on the ViewControllers group and select New File. Add a new Cocoa Touch Class that subclasses UIViewController named FeedViewController and ensure the language is set to Swift.
Open AppDelegate.swift and find application(_:didFinishLaunchingWithOptions:)
. Find the line that pushes ClassicFeedViewController()
onto the navigation controller, and replace it with this:
nav.pushViewController(FeedViewController(), animated: false)
FeedViewController
is now the root view controller. You’ll keep ClassicFeedViewController.swift around for reference, but FeedViewController
is where you’ll implement the new IGListKit-powered collection view.
Build and run and make sure a new, empty view controller shows up on screen.
Adding the Journal Loader
Open FeedViewController.swift and add the following property to the top of FeedViewController
:
let loader = JournalEntryLoader()
JournalEntryLoader
is a class that loads hard-coded journal entries into an entries
array.
Add the following to the bottom of viewDidLoad()
:
loader.loadLatest()
loadLatest()
is a JournalEntryLoader
method that loads the latest journal entries.
Adding the Collection View
It’s time to start adding some IGListKit-specific controls to the view controller. Before you do, you need to import the framework. Near the top of FeedViewController.swift, add a new import:
import IGListKit
#import
into your bridging header.
Add an initialized collectionView
constant to the top of FeedViewController
:
// 1
let collectionView: UICollectionView = {
// 2
let view = UICollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout())
// 3
view.backgroundColor = .black
return view
}()
Here’s what this code does:
- IGListKit uses a regular
UICollectionView
and adds its own functionality on top of it, as you will see later on. - Start with a zero-sized rect, since the view isn’t created yet. It uses a
UICollectionViewFlowLayout
just as theClassicFeedViewController
did. - Set the background color to NASA-approved black.
Add the following to the bottom of viewDidLoad()
:
view.addSubview(collectionView)
This adds the new collectionView
to the controller’s view.
Below viewDidLoad()
, add the following:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.frame = view.bounds
}
This overrides viewDidLayoutSubviews()
, setting the collectionView
frame to match the view
bounds.
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:
Adding Messages
JPL engineering is pretty happy that you got the refactor done so quickly, but they really need to establish communication with the stranded astronaut. They’ve asked you to integrate the messaging module ASAP.
Before you add any views, you first need the data.
Open FeedViewController.swift and add a new property to the top of FeedViewController
:
let pathfinder = Pathfinder()
PathFinder()
acts as a messaging system, and represents the physical Pathfinder rover the astronaut dug up on Mars.
Locate objects(for:)
in your ListAdapterDataSource
extension and modify the contents to match the following:
var items: [ListDiffable] = pathfinder.messages
items += loader.entries as [ListDiffable]
return items
You might recall that this method provides data source objects to your ListAdapter
. The modification here adds the pathfinder.messages
to items
to provide messages for a new section controller.
entries
array to make the Swift compiler happy. The objects already conform to IGListDiffable
.Right-click the SectionControllers group to create a new ListSectionController
subclass named MessageSectionController. Add the IGListKit import to the top:
import IGListKit
With the compiler happy, you’ll leave the rest unchanged for now.
Go back to FeedViewController.swift and update listAdapter(_:sectionControllerFor:)
in the ListAdapterDataSource
extension so it appears as follows:
if object is Message {
return MessageSectionController()
} else {
return JournalSectionController()
}
This now returns the new message section controller if the data object is of type Message
.
The JPL team wants you to set up MessageSectionController
with the following requirements:
- Receives a
Message
. - Has a bottom inset of 15 points.
- Returns a single cell sized using the
MessageCell.cellSize(width:text:)
method. - Dequeues and configures a
MessageCell
using theMessage
object’stext
anduser.name
values to populate labels. - Displays the
Message
object’suser.name
value in all capitals.
Give it a shot! The team drafted up a solution below in case you need help.
[spoiler title=”MessageSectionController”]
import IGListKit
class MessageSectionController: ListSectionController {
var message: Message!
override init() {
super.init()
inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
}
}
// MARK: - Data Provider
extension MessageSectionController {
override func numberOfItems() -> Int {
return 1
}
override func sizeForItem(at index: Int) -> CGSize {
guard let context = collectionContext else {
return .zero
}
return MessageCell
.cellSize(width: context.containerSize.width, text: message.text)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
let cell = collectionContext?
.dequeueReusableCell(of: MessageCell.self, for: self, at: index)
as! MessageCell
cell.messageLabel.text = message.text
cell.titleLabel.text = message.user.name.uppercased()
return cell
}
override func didUpdate(to object: Any) {
message = object as? Message
}
}
[/spoiler]
Once you’re ready, build and run to see messages integrated into the feed!
Weather on Mars
Your astronaut needs to be able to get the current weather in order to navigate around obstacles like dust storms. JPL built another module that displays the current weather. There’s a lot of information in there though, so they ask that the weather only display when tapped.
Create one last section controller named WeatherSectionController. Start the class off with an initializer and some variables:
import IGListKit
class WeatherSectionController: ListSectionController {
// 1
var weather: Weather!
// 2
var expanded = false
override init() {
super.init()
// 3
inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
}
}
What this code does:
- This section controller will receive a
Weather
object indidUpdate(to:)
. -
expanded
is aBool
used to track whether the astronaut has expanded the weather section. You initialize it tofalse
so the detail cells are initially collapsed. - Just like the other sections, use a bottom inset of 15 points.
Now add an extension to WeatherSectionController
and override three methods:
// MARK: - Data Provider
extension WeatherSectionController {
// 1
override func didUpdate(to object: Any) {
weather = object as? Weather
}
// 2
override func numberOfItems() -> Int {
return expanded ? 5 : 1
}
// 3
override func sizeForItem(at index: Int) -> CGSize {
guard let context = collectionContext else {
return .zero
}
let width = context.containerSize.width
if index == 0 {
return CGSize(width: width, height: 70)
} else {
return CGSize(width: width, height: 40)
}
}
}
Here’s how this works:
- In
didUpdate(to:)
, you save the passedWeather
object. - If you’re displaying the expanded weather,
numberOfItems()
returns five cells that will contain different pieces of weather data. If not expanded, you need only a single cell to display a placeholder. - The first cell should be a little larger than the others, as it displays a header. You don’t have to check the state of
expanded
because that header cell is the first cell in either case.
Next you need to implement cellForItem(at:)
to configure the weather cells. Here are some detailed requirements:
- The first cell should be of type
WeatherSummaryCell
, others should beWeatherDetailCell
. - Configure the weather summary cell with
cell.setExpanded(_:)
. - Configure four different weather detail cells with the following title and detail labels:
- “Sunrise” with
weather.sunrise
- “Sunset” with
weather.sunset
- “High” with
"\(weather.high) C"
- “Low” with
"\(weather.low) C"
- “Sunrise” with
Give this cell setup a shot. The solution is just below.
[spoiler title=”WeatherSectionController.cellForItem(at index:)”]
override func cellForItem(at index: Int) -> UICollectionViewCell {
let cellClass: AnyClass =
index == 0 ? WeatherSummaryCell.self : WeatherDetailCell.self
let cell = collectionContext!
.dequeueReusableCell(of: cellClass, for: self, at: index)
if let cell = cell as? WeatherSummaryCell {
cell.setExpanded(expanded)
} else if let cell = cell as? WeatherDetailCell {
let title: String, detail: String
switch index {
case 1:
title = "SUNRISE"
detail = weather.sunrise
case 2:
title = "SUNSET"
detail = weather.sunset
case 3:
title = "HIGH"
detail = "\(weather.high) C"
case 4:
title = "LOW"
detail = "\(weather.low) C"
default:
title = "n/a"
detail = "n/a"
}
cell.titleLabel.text = title
cell.detailLabel.text = detail
}
return cell
}
[/spoiler]
The last thing that you need to do is toggle the section expanded
and update the cells when tapped. Override another method from ListSectionController
:
override func didSelectItem(at index: Int) {
collectionContext?.performBatch(animated: true, updates: { batchContext in
self.expanded.toggle()
batchContext.reload(self)
}, completion: nil)
}
performBatch(animated:updates:completion:)
batches and performs updates in the section in a single transaction. You can use this whenever the contents or number of cells changes in the section controller. Since you toggle the expansion with numberOfItems()
, this will add or remove cells based on the expanded
flag.
Return to FeedViewController.swift and add the following near the top of FeedViewController
, with the other properties:
let wxScanner = WxScanner()
WxScanner
is the model object for weather conditions.
Next, update objects(for:)
in the ListAdapterDataSource
extension so that it looks like the following:
// 1
var items: [ListDiffable] = [wxScanner.currentWeather]
items += loader.entries as [ListDiffable]
items += pathfinder.messages as [ListDiffable]
// 2
return items.sorted { (left: Any, right: Any) -> Bool in
guard let
left = left as? DateSortable,
let right = right as? DateSortable
else {
return false
}
return left.date > right.date
}
You’ve updated the data source method to include currentWeather
. Here are details on what this does:
- Adds the
currentWeather
to the items array. - All the data conforms to the
DataSortable
protocol, so this sorts the data using that protocol. This ensures data appears chronologically.
Finally, update listAdapter(_:sectionControllerFor:)
to appear as follows:
if object is Message {
return MessageSectionController()
} else if object is Weather {
return WeatherSectionController()
} else {
return JournalSectionController()
}
This returns a WeatherSectionController
when a Weather
object appears.
Build and run again. You should see the new weather object at the top. Try tapping on the section to expand and contract it.
Performing Updates

JPL is ecstatic about your progress!
While you were working, the director of NASA coordinated a rescue operation for the astronaut, requiring him to launch and intercept with another ship! It’s going to be a complicated launch, so he’ll have to liftoff at precisely the right time.
JPL engineering extended the messaging module with real-time chat and they are asking you to integrate it.
Open FeedViewController.swift and add the following lines to the end of viewDidLoad()
:
pathfinder.delegate = self
pathfinder.connect()
The Pathfinder
module is all patched up with real-time support. All you need to do is connect to the unit and respond to delegate events.
Add the following extension to the bottom of the file:
// MARK: - PathfinderDelegate
extension FeedViewController: PathfinderDelegate {
func pathfinderDidUpdateMessages(pathfinder: Pathfinder) {
adapter.performUpdates(animated: true)
}
}
FeedViewController
now conforms to PathfinderDelegate
. The single method performUpdates(animated:)
tells the ListAdapter
to ask its data source for new objects and then update the UI. This handles objects that are deleted, updated, moved or inserted.
Build and run to see the captain’s messages updating! All you had to do was add one single method for IGListKit to determine what has changed in the data source and animate the changes when new data arrives:
All you need to do now is transmit the latest build to the astronaut and he’ll be coming home. A job well done!
Where to Go From Here?
You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.
Aside from bringing a stranded astronaut home, you’ve learned a lot about the basic features of IGListKit: section controllers, adapters and how to bring them all together. There are other important features in IGListKit like supplementary views and display events.
You can read and watch more about the origin of IGListKit at Instagram from a talk published by Realm. This talk covers a lot of the common UICollectionView
problems that apps experience as they get larger and more complex.
If you’re interested in helping contribute to IGListKit, the team set up starter-task tags on GitHub for an easy way to get started.
If you have any questions or comments about this tutorial or working with IGListKit in general, please join the forum discussion below!