Collection View Tetris
I’d like to tell the story of how exploring table view animations led me to this beautiful use of Xcode’s color literals.
A while back, I was investigating the animations in UITableView and UICollectionView. As you may be aware, in a table view you may do the following six kinds of animations:
- Inserting rows
- Deleting rows
- Moving rows
- Inserting sections
- Deleting sections
- Moving sections
The same set of animations, with items instead of rows, are available for collection views. These animations can also be batched, so that several operations are animated at once.
If you’ve ever used these methods, you may have become acquainted with the dreaded NSInternalInconsistencyException. The thing is, whenever you ask a table view to – for example – insert one row in a section, you have to make sure that when it calls your data source’s numberOfRows(inSection:)
, it has increased the value by one.
When you start out with table views and iOS programming, it may happen that you try to add logic in your data source methods that may look a little something like this:
func numberOfRows(inSection section: Int) -> Int {
if section == animalsSection {
return animals.count + self.isAddingAnimal ? 1 : 0
}
}
This kind of approach always ends in tears. It very quickly gets very hard to keep all the state consistent. Instead, you always want a model that represents the full state – an array, typically – and if you want to animate from one state to another, use a differ to calculate what rows and sections to insert, delete or move.
There are several good such tools around. They typically use the Longest Common Subsequence algorithm to perform the diff, which is what you should use for good performance characteristics.
What I became preoccupied with was the question of how to make such a diffing method that handled both items (rows) and sections and worked for any input. Basically, I wanted a function like:
func diff(from oldSections: [(SectionType, [ItemType])],
to newSections: [(SectionType, [ItemType])]) -> BatchChanges
And then this BatchChanges
would contain everything that you needed to send to the animation methods. It turned out that this was quite hard, because there were diffs that were impossible to perform in just one step – for example, when an item moves from one section to another, and that section also moves at the same time. Instead, you have to:
- first diff the sections from old to new,
- then apply that diff to your old items (let’s call the result of that the patched items),
- then diff your items from your patched items to the new items.
Then first perform the section animations – and while that happens make sure to return the per-section item counts from the patched items – then perform the item animations.
Phew.
This is boring, let’s play Tetris
So anyway, after I had my very general DataSource<SectionType, ItemType>
that could handle animation of changes from any state to any other, I wanted to do something crazy with it, to kind of see what you can do.
I got the idea of a Collection View Tetris. Here it is.
Here, every Tetris row is a collection view section consisting of ten cells. Each cell is a standard UICollectionViewCell
with only the background color set. The animations are really just cells switching places. And when you get Tetris, a section is deleted. But this all happens because of the differ – the Tetris game logic is written with no awareness of collection views.
I thought it was a fun hack. It’s been sitting around on my computer for a while now, waiting for me to do a presentation that never happened, so now I thought I’d just publish it. I don’t actually use the DataSource, because I haven’t needed anything more than just animating rows within a single section. Keep it simple, huh?
For all the fun, see the source on Github. The color literals? Unfortunately, they do not look as awesome in Github as they do in Xcode, and I don’t really recommend the use of them. Nor do I recommend that you use a UICollectionView for your next game. I do recommend having fun with things.
Update
Also see the Tetris Diffing Competition for a comparison on how other frameworks handle this task.