Learn & Master ⚔️ the Basics of IGListKit in 10 Minutes

Anyone who has ever implemented dynamic lists in iOS will probably know that the official approach has some difficulties. Datastore and view layer have to be synchronized manually and during the update a certain order should be kept to avoid crashes. In addition, the classical approach is tempting to encapsulate the logic of the different cells in a large data source rather than separating them sensibly. To prevent these problems IGListKit was created. A framework developed by the Instagram team . What exactly IGListkit does and how it can be used, I will show you in the following sections.

You can find the accompanying material for this post here: https://github.com/SebastianBoldt/Learn-And-Master-IGListKit

1. Connecting UICollectionView and ListAdapter

If you want to provide Data for an UICollectionView, you need a UICollectionViewDatasource. It tells the UICollectionView how many cells are displayed and how exactly these cells should be constructed. Functions such as numberOfItemsInSection, numberOfSections and cellForItemAtIndexPath are used for this purpose. If you have never heard of these before, you can read more about them here. The size of the cells is defined in a different class, which needs to conform to the UICollectionViewDelegate-Protocol. As a result, information that actually belongs together is distributed at different levels in the project.

IGListKit works in another way. The UICollectionView no longer sets a direct datasource, but only an adapter. This adapter manages the sections for us and provides further functions which we will investigate later on. The adapter has a datasource that tells it how exactly the cells are managed in the UICollectionView.

In the first step, a ListAdapter must be created. We generate it directly in the ViewController and configure the necessary properties.

class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
lazy var adapter: ListAdapter = {
let updater = ListAdapterUpdater()
let adapter = ListAdapter(updater: updater,
viewController: self,
workingRangeSize: 1)
        adapter.collectionView = collectionView
adapter.dataSource = SuperHeroDatasource()
return adapter
}()
    override func viewDidLoad() {
super.viewDidLoad()
_ = adapter
}
}

The adapter expects 3 properties: Updater, ViewController, and WorkingRangeSize.

  • updater - this object handles row and section updates. We use a default implementation here, that is suitable for our current usage. We will not talk about implementing a custom one in this tutorial.
  • viewController - this property can be used to navigate to other viewControllers later and it should be the viewController where the adapter is hosted in
  • workingRangeSize - A working range is a range of section controllers who aren’t yet visible, but are near the screen.

2. Creating Datamodels using ListDiffable

Next we have to create the actual data models. IGListkit uses a protocol named ListDiffable for that. ListDiffable allows IGListKit to uniquely identify and compare objects. Somehow similar to the Hashable or Equatable protocols known from the Swift environment. Internally it uses this functionality to automatically update the data inside the UICollectionView.

In the following code sample we will create a simple model for Superheroes and implement the mandatory functions of the Diffable protocol. Later on we will display these Elements using custom cells inside a UICollectionView with the help of IGListKit.

class SuperHero {
private var identifier: String = UUID().uuidString
private(set) var firstName: String
private(set) var lastName: String
private(set) var superHeroName: String
private(set) var icon: String
    init(firstName: String, 
lastName: String,
superHeroName: String,
icon: String) {
self.firstName = firstName
self.lastName = lastName
self.superHeroName = superHeroName
self.icon = icon
}
}
extension SuperHero: ListDiffable {
func diffIdentifier() -> NSObjectProtocol {
return identifier as NSString
}

func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
guard let object = object as? SuperHero else {
return false
}
    return self.identifier == object.identifier
}
}

A Diffable Object has to implement the following two functions:

  • diffIdentifier -returns a unique object that can be used to compare and identify our model.
  • isEqual -will be used when comparing two objects with each other

That’s it, our model can now be used with IGListKit.

3. Providing Data using

To ensure that the ListAdapter knows which data it is responsible for and how to display it, it needs some kind of Datasource. In IGListKit the Datasource is an Object that must conform to the ListAdapterDataSourceProtocol.

In the following code snippet we create a new class which holds the data and defines its formatting in the form of cells by returning a suitable ListSectionController. The exact function of the ListSectionController is explained in the next section 😅.

class SuperHeroDataSource: NSObject, ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return [SuperHero(firstName: "Peter",
lastName: "Parker",
superHeroName: "SpiderMan",
icon: "🕷"),
            SuperHero(firstName: "Bruce", 
lastName: "Wayne",
superHeroName: "Batman",
icon: "🦇"),
            SuperHero(firstName: "Tony", 
lastName: "Stark",
superHeroName: "Ironman",
icon: "🤖"),
            SuperHero(firstName: "Bruce", 
lastName: "Banner",
superHeroName: "Incredible Hulk",
icon: "🤢")]
}
  func listAdapter(_ listAdapter: ListAdapter, 
sectionControllerFor object: Any) -> ListSectionController {
return SuperHeroSectionController()
}
  func emptyView(for listAdapter: ListAdapter) -> UIView? {
return nil
}
}
  • objects(for listAdapter: ListAdapter) -> [ListDiffable] 
    - Returns the objects that should be managed by the Adapter.
  • listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> SectionController
    -
    returns a SectionController for a specific model
  • emptyView(for listAdapter: ListAdapter) -> UIView?
    -
    Returns a view that is displayed if no objects exist

4. Configuring Cells

Within the SuperHeroDataSource, we create a single ListSectionController but we could also return a appropriate ListSectionController for a particular model type if we want to. This means that we are able to provide different configurations for different models in the form of separate classes. This is a massive advantage over the well-known approaches. This subdivision takes the form of a ListSectionController, in our Example the SuperHeroSectionController, which is responsible for the creation and configuration of one or more cells based on one single ListDiffable Model 😲.

class SuperHeroSectionController: ListSectionController {
var currentHero: SuperHero?
override func didUpdate(to object: Any) {
guard let superHero = object as? SuperHero else {
return
}
    currentHero = superHero
}

override func numberOfItems() -> Int {
return 1 // One hero will be represented by one cell
}
  override func cellForItem(at index: Int) -> UICollectionViewCell {
let nibName = String(describing: SuperHeroCell.self)

guard let ctx = collectionContext, let hero = currentHero else {
return UICollectionViewCell()
}

let cell = ctx.dequeueReusableCell(withNibName: nibName,
bundle: nil,
for: self,
at: index)
    guard let superHeroCell = cell as? SuperHeroCell else {
return cell
}
superHeroCell.updateWith(superHero: hero)
return superHeroCell
}
  override func sizeForItem(at index: Int) -> CGSize {
let width = collectionContext?.containerSize.width ?? 0
return CGSize(width: width, height: 50)
}
}
  • didUpdate(to object: Any)
    - W
    ill be called if the sectionController gets its data. We just use this function to store our current model.
  • cellForItem(at index: Int) -> UICollectionViewCell
    -
    This should sound familiar. We dequeue our cell and configure it using the data model. Keep in mind that you need to use a special context object instead of the actual collectionView to retrieve a cell from the Reusepool.
  • sizeForItem(at index: Int) -> CGSize
    - Return the size for cell at the given index. We use the context here again to retrieve the containerviews size.
  • numberOfItem() -> Int
    -
    This is where things got really interesting for me. You may ask yourself why we are returning just a count of one? This is because our one viewModel will be handled by a single ListSectionController, and our viewModel should just be represented by one cell in the final list. 
    In the next chapter I will show you how to display multiple cells for one item.

Compile & Run.

5. One Item, multiple Cells

As you should already know, IGListKit can generate multiple cells from a single model. In addition to that, the ListSectionController makes it incredibly easy to fade in and out certain cells. Therefore, we will extend the project in the following so that when you click on the cell, another cell will be extended to reveal the actual superhero behind the name 😱.

In the first step we have to give the SuperHeroSectionController another property called collapsed. This property will be toggled when tapping the cell. Nothing could be easier:

class SuperHeroSectionController: ListSectionController {
var currentHero: SuperHero?
var collapsed: Bool = true
...
}
}
extension SuperHeroSectionController {
override func didSelectItem(at index: Int) {
self.collapsed = !self.collapsed
}
}

We can now use this property to configure our cells inside the sectioncontrollers functions. First, we will make the number of cells depend on the value of collapased. If the value is true, we only want to display the cell that shows the first and last name. If the value is false, an additional cell with the superhero identity should appear.

class SuperHeroSectionController: ListSectionController {
...

override func numberOfItems() -> Int {
return collapsed ? 1 : 2
}
}

In cellForItem the corresponding cell can now be created and initialized with the model depending on the index. I created a new cell Type and some Helper-Functions for that. For the complete Implementation please take a look the Github Project over here.

enum Index: Int {
case realName
case superHeroName
}
class SuperHeroSectionController: ListSectionController {
override func cellForItem(at index: Int) -> UICollectionViewCell {
var cell: UICollectionViewCell = UICollectionViewCell()
guard let hero = currentHero else {
return cell
}
    switch index {
case Index.realName.rawValue:
if let realNameCell = getRealnameCell(at: index){
cell = realNameCell
}
case Index.superHeroName.rawValue:
if let superHeroNameCell = getSuperHeroNameCell(at: index) {
cell = superHeroNameCell
}
default: ()
}
    guard let superHeroCell = cell as? SuperHeroModelUpdatable else   
{
return cell
}
superHeroCell.updateWith(superHero: hero)
return cell
}
}

The last thing we need to do is to actually update the UICollectionView when the cell is pressed. We can achieve this by calling performBatch on the context object of the SuperHeroSectionController and calling the reload it function inside the updates block.

extension SuperHeroSectionController {
override func didSelectItem(at index: Int) {
collectionContext?.performBatch(animated: true, updates: {
(batchContext) in
self.collapsed = !self.collapsed
batchContext.reload(self)
})
}
}

Cool. Now we are able to show and hide the super-heros identity by tapping on the cell.

6. Multiple Sectioncontrollers

Separate Sectioncontrollers can be created for each model type. Depending on the type, the corresponding sectioncontroller is simply returned in the data source. So if you want to display an advertising banner between the superhero cells, you could achieve this by creating a new model called Ad which obviously needs to be a Diffable Object.

class Ad: ListDiffable {
private var identifier: String = UUID().uuidString
private(set) var description: String
init(description: String) {
self.description = description
}

func diffIdentifier() -> NSObjectProtocol {
return identifier as NSString
}
  func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
guard let object = object as? Ad else {
return false
}
return self.identifier == object.identifier
}
}

The next thing we need to do is to create a new ListSectionController called AdSectionController, which is basically build the same way as the SuperHeroSectionController above. The only difference is that it’s using a different cell and model type.

class AdSectionController: ListSectionController {
var currentAd: Ad?
override func didUpdate(to object: Any) {
guard let ad = object as? Ad else {
return
}
currentAd = ad
}

override func numberOfItems() -> Int {
return 1
}
  override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let ctx = collectionContext else {
return UICollectionViewCell()
}
let nibName = String(describing: AdCell.self)
let cell = ctx.dequeueReusableCell(withNibName: nibName,
bundle: nil,
for: self, at: index)
    if let ad = currentAd {
(cell as? AdCell)?.updateWithAd(ad: ad)
}
return cell
}
  override func sizeForItem(at index: Int) -> CGSize {
let width = collectionContext?.containerSize.width ?? 0
return CGSize(width: width, height: 30)
}
}

Inside our Datasource we now need to return the new AdSectionController in the sectionControllerFor function of the SuperHeroDataSource if the Object is an Ad.

class SuperHeroDataSource: NSObject, ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return [SuperHero(firstName: "Peter",
lastName: "Parker",
superHeroName: "SpiderMan",
icon: "🕷"),
            Ad(description: "Want to know your future? 🔮"),
            SuperHero(firstName: "Bruce", 
lastName: "Wayne",
superHeroName: "Batman",
icon: "🦇"),
            SuperHero(firstName: "Tony", 
lastName: "Stark",
superHeroName: "Ironman",
icon: "🤖"),
            SuperHero(firstName: "Bruce", 
lastName: "Banner",
superHeroName: "Incredible Hulk",
icon: "🤢")]
}
  func listAdapter(_ listAdapter: ListAdapter, 
sectionControllerFor object: Any) -> ListSectionController {
if object is SuperHero {
return SuperHeroSectionController()
} else {
return AdSectionController()
}
}
}

Compile & Run.

We now see an AdCell managed by the new AdSectionController.

It’s a wrap 🎁

I think this Article is a good and simple introduction into the World if IGlistKit. Of course, the framework can do even more, but I think that the basis for understanding more complex functions has been laid.

If the article has helped you and you want me to continue writing similar articles, you are welcome to support me with a small donation

🤜🏾 🤛

Congratulation, you learned the basics of IGListKit. Happy Coding 🎉


Feel free to add me on github, twitter, linkedin or xing if you have any questions. If you like electronic music you can also listen to my Tracks on SoundCloud ;)