Mobile Development 15 min read

Using IGListKit for Efficient List Management in iOS Applications

This article explains how IGListKit can replace UICollectionView for building list scenes in iOS apps, detailing its architecture, adapter‑section controller pattern, diff‑based updates, practical issues such as cell‑level diffing, and integration with RxSwift to achieve smooth, performant UI rendering.

ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
Using IGListKit for Efficient List Management in iOS Applications

Introduction

Most iOS apps implement list screens with UICollectionView . Although powerful, it has drawbacks such as screen flicker when calling reloadData and the difficulty of writing custom updaters for performBatchUpdates(_:completion:) . Our Intelligent Client team evaluated these problems and chose IGListKit for all list scenarios in the Dali Together Learning HD project.

Overview of IGListKit

The architecture of IGListKit replaces the direct data source of UICollectionView with an adapter . For each object type, an appropriate section controller is created, which builds and maintains the corresponding cell. Each section controller also acts as a section view, managing supplementary and decoration views.

Usage

First create an adapter (usually in the view controller) and set its data source and updater .

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 requires three properties:

updater : handles row and section updates (default implementation is usually sufficient).

view controller : the object that owns the adapter.

workingRangeSize : defines a range of sections that are near the screen edge for pre‑loading content.

Next, define a data model that conforms to ListDiffable :

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 other = object as? SuperHero else { return false }
        return self.identifier == other.identifier
    }
}

Implement a data source that returns an array of ListDiffable objects and provides a section controller for each object:

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 }
}

The corresponding section controller implements the lifecycle methods needed to display a cell:

class SuperHeroSectionController: ListSectionController {
    var currentHero: SuperHero?

    override func didUpdate(to object: Any) {
        guard let hero = object as? SuperHero else { return }
        currentHero = hero
    }

    override func numberOfItems() -> Int { return 1 }

    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) as? SuperHeroCell ?? UICollectionViewCell()
        cell.updateWith(superHero: hero)
        return cell
    }

    override func sizeForItem(at index: Int) -> CGSize {
        let width = collectionContext?.containerSize.width ?? 0
        return CGSize(width: width, height: 50)
    }
}

Principles

IGListKit performs diffing by comparing the previous and new data sources, calculating insert , remove , update , and move operations, and then calls UICollectionView.performBatchUpdates(_:completion:) . The diff algorithm uses diffIdentifier for identity and isEqual(toDiffableObject:) to detect content changes.

Practice

Two common issues were encountered when using IGListKit.

Diff‑level Updates

By default IGListKit diffing occurs at the section level, causing whole sections to reload even when only a single cell's data changes. To achieve cell‑level diffing, the data source can be flattened so that each section controller manages exactly one item, establishing a 1:1 section‑to‑cell relationship.

When a layout requires two cells side‑by‑side, using two sections with the default UICollectionViewFlowLayout is not feasible because sections span the full cross‑axis width. Switching to ListCollectionViewLayout solves the problem but limits custom layout flexibility.

Instead, ListBindingSectionController can be used to obtain true cell‑level diffing. The data model must still conform to ListDiffable , but the isEqual(toDiffableObject:) implementation can simply return true to force IGListKit to treat every change as an update that will be handled by RxSwift.

func isEqual(toDiffableObject object: IGListDiffable?) -> Bool { return true }

A binding section controller implements three data‑source methods to supply view models, cells, and sizes:

func sectionController(_ sectionController: IGListBindingSectionController, viewModelsFor object: Any) -> [IGListDiffable] { ... }
func sectionController(_ sectionController: IGListBindingSectionController, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell { ... }
func sectionController(_ sectionController: IGListBindingSectionController, sizeForViewModel viewModel: Any, at index: Int) -> CGSize { ... }

Integration with RxSwift

Because IGListKit is data‑driven but not bidirectional, any mutation must result in a new data‑source instance so that the diff engine can detect changes. Using structs makes in‑place mutation impossible; switching to classes enables reference‑based updates, but the same instance would still hide changes from IGListKit.

The recommended flow is:

Element notifies the view model of the field to modify.

View model finds the element’s index and records it.

View model rebuilds a new data source, applying the modification at the recorded index.

The new data source is handed to IGListKit, which performs the diff update.

To let RxSwift handle the actual UI update, the model’s isEqual(toDiffableObject:) can be forced to return true , delegating the visual change to a subscription in the cell. The cell must dispose of the subscription in prepareForReuse :

class ModelCell: UICollectionReusableView {
    let label = UILabel()
    var bag = DisposeBag()
    override func prepareForReuse() {
        super.prepareForReuse()
        bag = DisposeBag()
    }
    func bindViewModel(model: Model) {
        model.field
            .subscribe(onNext: { [weak self] text in
                self?.label.text = text
            })
            .disposed(by: bag)
    }
}

Conclusion

By studying IGListKit’s source code and understanding its diffing mechanism, we resolved two typical practical problems: achieving cell‑level updates and integrating reactive streams with RxSwift. Proper use of IGListKit leads to cleaner business logic, smooth default animations, and excellent performance, making it a recommended choice for iOS list implementations.

Click Read the original article to discover more technical insights~

iosSwiftDiffable Data SourceRxSwiftIGListKitList Management
ByteDance Dali Intelligent Technology Team
Written by

ByteDance Dali Intelligent Technology Team

Technical practice sharing from the ByteDance Dali Intelligent Technology Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.