UICollectionView: How to easily deal with updates
When you are using CollectionViews or TableViews, the difficult part is when you need to add, remove, move some cells or update their content.
Even more when you are using decoration views.
Introduction
Look at these 2 arrays:
let old= ["A","B"]
let new= ["B","C"]
The differences between these arrays is very straight forward to compute:
deleted : [0] // index in the old array
moved : [(from:1, to:0)]
inserted : [1] // index in the new array
reloaded : []
To compute this diff, we use a key to compare the two arrays, which is the value in the array in this case. With key, we can compute moves, deletes and inserts.
- the key A was deleted of position 0
- the key B moved from position 1 to position 0
- the key C was inserted at position 1
It’s become more complicated when we want to compute reloaded indices between two arrays. We also need a key, and a value. If the value changes, we need to reload the item.
let old = [(key:"A",value:0), (key:"B",value:0)]
let new = [(key:"C",value:0), (key:"B",value:1)]
The result of the diff:
deleted : [0] // key A deleted
moved : []
inserted : [0] // key C inserted
reloaded : [1] // value of B has changed
Perfect, we can now swap Int
indices by IndexPath
indices:
let old = [
(key:"A", value:0, index:(section:0, item:0),
(key:"B", value:0, index:(section:0, item:1))
]
let new = [
(key:"C", value:0, index:(section:0, item:0)),
(key:"B", value:1, index:(section:0, item:1))
(key:"D", value:0, index:(section:0, item:2))
]// diffing
deleted : [(section:0, item:0)] // key A deleted
moved : []
inserted : [(section:0, item:0), (section:0, item:2)] // keys C & D
reloaded : [(section:0, item:1)] // value of B has changed
With that, we can easily compute diff between twos collectionView dataSources!
Let’s start with a little sample: a real time “tweeter” app built with a custom layout. Each 2 seconds, the app receives a new feed and updates its collectionView. Each “tweet” is represented by a section, a cell for the content, a cell for the number of retweets and a cell for the favorites count. Retweet and favorites cells are only displayed if their count is positive.
For building collectionView, I’m using Collor, a micro architecture for simplifying collectionView implementation.
More info in a previous article here. I recommend to read it before continuing reading.
There is the collectionData of the collectionView, one readable file, which describes the collectionView, it’s the model of the collectionView:
Diffing
We use the Collor diffing feature to compute differences between feeds. It’s a big feature because, as a developper, you no longer have to manually update the collectionview if your model changes. Collor does it for you.
Each time the app receives a new feed, the collectionData is updated with the new data then the diff between the old and the new collectionData is computed and Collor returns the removed, inserted and reloaded indexPaths (the struct result). The collectionView is also updated with “beautiful” animations which help the user to understand what’s happening in you app.
Even if collectionview update animations are not gorgeous, it’s better than a classic reloadData().
In Collor, to compute inserts and deletes, you just need to add a unique key to descriptors, represented by uid()
method:
// section
TweetSectionDescriptor().uid(tweet.id)
// items
TweetDescriptor(adapter: TweetAdapter(tweet: tweet)).uid("text")TweetInfoDescriptor(adapter: RetweetAdapter(tweet: tweet)).uid("retweet")TweetInfoDescriptor(adapter: FavoriteAdapter(tweet: tweet)).uid("favorite")
But, for computing reloads, Collor needs more information, it needs to know the “value” of the descriptor. This value is the adapter. But in Collor, CollectionAdapter
protocol is not conform to Equatable, for many reasons. So, if you want to compute reloads, you have to make yours adapters conform to Diffable
. If you don’t, because for some good reasons, you can use the key as value, the cell will be also deleted then re-inserted:
TweetDescriptor(adapter: TweetAdapter(tweet: tweet))
.uid("text_\(tweet.text)")
The animation of reload is different from delete/insert:
In our example, we decided to compute reloads, so we make our adapters conform to Diffable
by implementing the isEqual(to other: Diffable?) -> Bool
method:
During diffing calculation, Collor will test, when keys and index are equal, if value has changed or not. If yes, the item will be reloaded.
Custom Layout
If you are using decoration views in custom layouts, you already noticed coding that can be really redundant. Furthermore, you also have to handle collectionView updates and manually delete or insert decoration views.
Collor may help you by using theDecorationViewHandler
feature.
DecorationViewHandler
allows you to:
- Register view class or xib
- cache then return decoration
UICollectionViewLayoutAttributes
- Return attributes in rect
- Manages collectionView updates ❤️
DecorationViewsHandler reduces the number of lines of code and errors and manage for you updates! According updateItems
it determines what decoration views to delete or insert.
UICollectionView is reusing views for memory optimisations. That’s why animations may seem strange sometimes: instead of creating a new background view (what a human would do), UICollectionView reuses another and move it from it’s old position to the new. You can improve this behaviour by overriding these methods:
- initialLayoutAttributesForAppearingDecorationElement(ofKind:at:)
- finalLayoutAttributesForDisappearingDecorationElement(ofKind:at:)
Conclusion
By using Collor for building your collectionViews, you no longer need to manually implement updates, either at the cell level or at decoration view level. Instead of reloading the whole collectionView and losing the user, you are able to show to the user what changes are happening in your app!
Have a look at the realtime sample on github.
Thanks for reading, and let me know what you think on Twitter.