/IGListKit

A data-driven UICollectionView framework for building fast and flexible lists.

Primary LanguageObjective-COtherNOASSERTION

Build Status Version Status license BSD Platform Carthage compatible


A data-driven UICollectionView framework for building fast and flexible lists.

     | Main Features

---------|--------------- 🙅 | Never call performBatchUpdates(_:, completion:) or reloadData() again 🏠 | Better architecture with reusable cells and components 🔠 | Create collections with multiple data types 🔑 | Decoupled diffing algorithm ✅ | Fully unit tested 🔍 | Customize your diffing behavior for your models 📱 | Simply UICollectionView at its core 🚀 | Extendable API 🐦 | Written in Objective-C with full Swift interop support

IGListKit is built and maintained by Instagram engineering, using the open source version for the Instagram app.

Installation

CocoaPods

The preferred installation method for IGListKit is with CocoaPods. Simply add the following to your Podfile:

# Latest release of IGListKit
pod 'IGListKit'

Carthage

To integrate IGListKit into your Xcode project using Carthage, specify it in your Cartfile:

github "Instagram/IGListKit" ~> 1.0.0

Manually

You can also manually install the framework by dragging and dropping the IGListKit.xcodeproj into your workspace.

IGListKit supports a minimum iOS version of 8.0.

Creating your first list

After installing IGListKit, creating a new list is really simple.

Creating a section controller

Creating a new section controller is very simple. You just subclass IGListSectionController and conform to the IGListSectionType protocol. Once you conform to IGListSectionType, the compiler will make sure you implement all of the required methods.

Take a look at LabelSectionController for an example section controller that handles a String and configures a single cell with a UILabel.

class LabelSectionController: IGListSectionController, IGListSectionType {
  // ...
}

Creating the UI

After creating at least one section controller, you must create an IGListCollectionView and IGListAdapter.

let layout = UICollectionViewFlowLayout()
let collectionView = IGListCollectionView(frame: CGRect.zero, collectionViewLayout: layout)

let updater = IGListAdapterUpdater()
let adapter = IGListAdapter(updater: updater, viewController: self, workingRangeSize: 0)
adapter.collectionView = collectionView

Note: This example is done within a UIViewController and uses both a stock UICollectionViewFlowLayout and IGListAdapterUpdater. You can use your own layout and updater if you need advanced features!

Connecting a data source

The last step is the IGListAdapter's data source and returning some data.

func objectsForListAdapter(listAdapter: IGListAdapter) -> [IGListDiffable] {
  // this can be anything!
  return [ "Foo", "Bar", 42, "Biz" ]
}

func listAdapter(listAdapter: IGListAdapter,
    sectionControllerFor object: Any) -> IGListSectionController {
  if object is String {
    return LabelSectionController()
  } else {
    return NumberSectionController()
  }
}

func emptyViewForListAdapter(listAdapter: IGListAdapter) -> UIView? {
  return nil
}

You can return an array of any type of data, as long as it conforms to IGListDiffable. We've included a default implementation for all objects, but adding your own implementation can unlock even better diffing.

Diffing

IGListKit uses an algorithm adapted from a paper titled A technique for isolating differences between files by Paul Heckel. This algorithm uses a technique known as the longest common subsequence to find a minimal diff between collections in linear time O(n). It finds all inserts, deletes, updates, and moves between arrays of data.

To add custom, diffable models, you need to conform to the IGListDiffable protocol and implement diffIdentifier() and isEqual(_:).

For an example, consider the following model:

class User {
  let primaryKey: Int
  let name: String
  // implementation, etc
}

The user's primaryKey uniquely identifies user data, and the name is just the value for that user.

Let's say a server returns a User object that looks like this:

let shayne = User(primaryKey: 2, name: "Shayne")

But sometime after the client receives shayne, someone changes their name:

let ann = User(primaryKey: 2, name: "Ann")

Both shayne and ann represent the same unique data because they share the same primaryKey, but they are not equal because their names are different.

To represent this in IGListKit's diffing, add and implement the IGListDiffable protocol:

extension User: IGListDiffable {
  func diffIdentifier() -> NSObjectProtocol {
    return primaryKey
  }

  func isEqual(object: Any?) -> Bool {
    if let object = object as? User {
      return name == object.name
    }
    return false
  }
}

The algorithm will skip updating two User objects that have the same primaryKey and name, even if they are different instances! You now avoid unecessary UI updates in the collection view even when providing new instances.

Note: Remember that isEqual(_:) should return false when you want to reload the cells in the corresponding section controller.

Diffing outside of IGListKit

If you want to use the diffing algorithm outside of IGListAdapter and UICollectionView, you can! The diffing algorithm was built with the flexibility to be used with any models that conform to IGListDiffable.

let result = IGListDiff(oldUsers, newUsers, .equality)

With this you have all of the deletes, reloads, moves, and inserts! There's even a function to generate NSIndexPath results.

Advanced Features

Working Range

A working range is a distance before and after the visible bounds of the UICollectionView where section controllers within this bounds are notified of their entrance and exit. This concept lets your section controllers prepare content before they come on screen (e.g. download images).

The IGListAdapter must be initialized with a range value in order to work. This value is a multiple of the visible height or width, depending on the scroll-direction.

let adapter = IGListAdapter(updater: IGListAdapterUpdater(),
                     viewController: self,
                   workingRangeSize: 0.5) // 0.5x the visible size

working-range

You can set the weak workingRangeDelegate on an section controller to receive events.

Supplementary Views

Adding supplementary views to section controllers is as simple as setting the weak supplementaryViewSource and implementing the IGListSupplementaryViewSource protocol. This protocol works nearly the same as returning and configuring cells.

Display Delegate

Section controllers can set the weak displayDelegate delegate to an object, including self, to receive display events about a section controller and individual cells.

Custom Updaters

The default IGListAdapterUpdater should handle any UICollectionView update that you need. However, if you find the functionality lacking, or want to perform updates in a very specific way, you can create an object that conforms to the IGListUpdatingDelegate protocol and initialize a new IGListAdapter with it.

Check out the updater IGListReloadDataUpdater (used in unit tests) for an example.

Documentation

You can find the docs here. Documentation is generated with jazzy and hosted on GitHub-Pages.

Contributing

Please see the CONTRIBUTING file for how to help out. At Instagram we sync the open source version of IGListKit almost daily, so we're always testing the latest changes. But that requires all changes be thoroughly tested follow our style guide.

License

IGListKit is BSD-licensed. We also provide an additional patent grant.

The files in the /Example directory are licensed under a separate license as specified in each file; documentation is licensed CC-BY-4.0.