Collor: a MVVM data-oriented framework for UICollectionView

The problem

At voyages-sncf.com, the main app for booking train tickets in France and Europe, our main job is to display what the server sends to us, most of views are also dynamic. Furthermore, the app is often updated with new features, even static views evolve constantly.

So, we decided to use UICollectionView! It’s powerful, dynamic and customisable.

But the Apple’s indexPath oriented implementation could become messy when there is a lot of different items, like in the Voyages-sncf.com app:
Code duplication, switch case, difficult to maintain, risks of indexPath errors during updates, …

Solution

We have built a framework for one year for simplifying and accelerating our development of theses UICollectionView screens. The main goal of this library is to have in one file, a readable dataSource which represents the collectionView content.

We called it Collor, Coll[ectionViewDescript]or.

https://github.com/voyages-sncf-technologies/Collor

Collor makes two things:

  • First, it provides a scalable micro architecture based on MVVM to organise the code and to avoid duplications.
  • Then, it provides some features in order to remove a lot of code needed by collectionView implementation like cell registering among other things, and to easily update the collectionView: deletion, insertion, reloading, diffing, ….

Architecture

To describe the collectionView, a collectionData object, which inherits from CollectionData is used. It is divided in section and items.

The UICollectionView dataSource asks the the collectionData to know the number of sections, the number of items in a section and what cell to dequeue.

A collectionData object contains an array of SectionDescriptors: SectionDescriptor implements the protocol SectionDescribable and handles some section features like sectionInset, minimumInteritemSpacing and minimumLineSpacing using by Apple’s FlowLayout. SectionDescriptor contains also an array of CellDescriptors.

CellDescriptor is the object which describes the collectionViewCell. It implements the protocol CollectionCellDescribable. It contains an adapter which transforms the model in a readable data and which is used by the UICollectionViewCell to display the data. CellDescriptor also handles the size of the cell.

Example of TitleAdapter. An adapter is most of cases a struct, it doesn’t need inheritance and is lightweight.

Finally, CollectionViewCell implements the protocol CollectionCellAdaptable and does nothing but makes the link between the adapter and the IBOutlets. You could use Collor with our legacy code thanks to swift extension.

For summarising, to display one cell, 4 objects are needed:

  • 1 cellDescriptor
  • 1 adapter
  • 1 UICollectionViewCell (swift file + xib)

You may think it’s hard work, each time you need to create a cell, you have to create 4 files ? No, most of cases, once a cell and its descriptor are created, only creating a new adapter is required if your cell is well designed. It’s the purpose of Collor: reuse a maximum of things.

Furthermore, Collor comes with Xcode some file templates, which help to create cells and theirs associated files (xib, view, adapter, descriptor), sections and even viewControllers, see here how to install it.

Reusability

The goal of Collor is to limit the creation of cells by re-using existing code. For one type of cell, there should be a view, a descriptor and an adapter protocol.

The same collectionViewCell used in different screens

For example, on the Voyages-sncf.com app, each time we need a simple label cell, we just create a new adapter which implements the protocol VSCollectionLabelAdapter and we reuse both the UICollectionViewCell and its descriptor created previously.

The adapter manages the style of the label ; the label fills the whole cell using Autolayout and its height is calculated using NSAttributedString.boundingRect().

Some code for explaining what we did:

Scalability

Collor is scalable, by example, in some cases, the cell width, the cell height or both could be defined in the adapter, or even the cell identifier and the cell className.

Two cells, two adapters but one descriptor

Indeed, you may have a descriptor which handles similar cells with minimal changes: an image on the left or on the right. So there will be two cells, but one descriptor and a common adapter protocol.

Finally, descriptors can carry the UI part of the collectionView by settings some parameters which will be used by decoration views in your custom collectionView layout.

Each section has a background view handled by the collectionView Layout

Collor is a protocol oriented framework. Instead of adding a property in the section descriptor, it could implement a new protocol BackgroundDrawable by example, so it can be reused in other custom layouts…

protocol BackgroundDrawable {
var backgroundInset: UIEdgeInsets { get set }
}

CollectionView Updates

If you already had to implement an expand/collapse in a tableView or collectionView, Collor should interest you.

Indeed, Collor provides some methods to add, remove, reload sections or items easily. Working with IndexPath is no more needed, you just manipulate descriptor references and Collor does the job. After updating the collectionData, Collor gives you a result object you use to update the collectionView by calling UICollectionView.performBatchUpdates(_:completion:) or UICollectionView.performUpdates(with:completion:). The data and the view are also always synchronised with the collectionView.

let result = myCollectionData.update { updater in
updater.remove(cells: [cellDescriptor])
updater.append(sections: [blueSection], after: lastSection!)
// ...
}
collectionView.performUpdates(with: result)

Each update in your collectionData must be enclosed in the closure given by CollectionData.update(_:)

Have a look at the pantone sample on github.

Diffing

Diff Data

With Collor, it’s possible to compute the diff between two datas. Collor is using the amazing Dwifft library by Jack Flintermann to do that. It’s very useful if your data contains conditional displays.

Compute differences between two data states with a few lines of code:

myModel.someChanges()
yourCollectionData.update(model: myModel)
let result = yourCollectionData.update{ updater in
updater.diff()
}
collectionView.performUpdates(with: result)

For using this feature, each section and each item must have an unique identifier, a string value:

let yellowSection = MainColorSectionDescriptor().uid("yellowSection")
let yellowTitle = TitleDescriptor(adapter: TitleAdapter(color: .yellow)).uid("yellowTitle")

Cells in different section can have a same id, when computing diffs, cell uid is concatenated with its section uid. In the previous example, yellowTitle uid is yellowSection/yellowTitle.

Have a look at the random sample on github.

Diff Section

You may just make some changes in a single section. By example an expand collapse. It’s pointless to compute diffing on the entire data.

Collor provides a feature to just compute differences in some sections:

sectionDescriptor.isExpanded = !sectionDescriptor.isExpanded
let result = collectionData.update{ updater in
updater.diff(sections: [sectionDescriptor])
}
collectionView.performUpdates(with: result)

It’s because of this feature that each section building is enclosing in a closure. Collor can also have the old and the new state of the sectionDescriptor cells array.

let section = MySectionDescriptor().reloadSection { cells in
cells.append(...)
}

Have a look at the weather sample on github.

Thanks for reading, and let me know what you think on Twitter.