Collor: a MVVM data-oriented framework for UICollectionView
The problem
At oui.sncf, 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.
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.
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.
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.