I'm working on the iOS version of an app I already developed on Android. This app has the following 2 column grid of self-sizing (fixed width but variable height) cells:
Achieving this in the Android version was easy because Google provides a StaggeredGridLayoutManager
for its RecyclerView
. You specify the number of columns and the direction of the scroll and you are done.
The default UICollectionView
layout UICollectionViewFlowLayout
doesn't allow the staggered layout I'm looking for, so I have to implement a custom layout. I have watched 2 WWDC videos that talk about this topic (What's New in Table and Collection Views and Advanced User Interfaces with Collection Views) and I more or less have an idea of how it should be implemented.
Step 1. First an approximation of the layout is computed.
Step 2. Then the cells are created and sized with autolayout.
Step 3. Then the controller notifies the of the cell sizes so the layout is updated.
My doubts come when trying to code these steps. I found a tutorial that explains the creation of a custom layout with staggered columns, but it doesn't use autolayout to obtain the size of the cells. Which leaves me with the following questions:
In step 2, how and when can I obtain the cell size?
In step 3, how and when can I notify the layout of the changes?
I want to point out that, as you have mentioned, RayWenderlich PinInterest Layout is exactly the tutorial that'll help you achieve this layout.
To answer your questions - with regards to the tutorial:
In step 2, how and when can I obtain the cell size?
To get the cell height, a delegate method was implemented that was called in the prepareLayout
method of the custom UICollectionViewLayout
. This method is called once (or twice, I just attempted to run it with a print
statement, and I got two calls). The point of prepareLayout
is to initialize the cell's frame
property, in other words, provide the exact size of each cell. We know that the width is constant, and only the height
is changing, so in this line of prepareLayout
:
let cellHeight = delegate.collectionView(collectionView!,
heightForItemAtIndexPath: indexPath, withWidth: width)
We obtain the height of the cell from the delegate method that was implemented in the UICollectionViewController
. This happens for all the cells we want to show in the collectionView
. After obtaining and modifying the height for each cell, we cache the result so we can inspect it later.
Afterwards, for the collectionView
to obtain the size of each cell on screen, all it needs to do is query the cache for the information. This is done in layoutAttributesForElementsInRect
method of your custom UICollectionViewLayout
class.
This method is called automatically by the UICollectionViewController
. When the UICollectionViewController
needs layout information for cells that are coming onto the screen (as a result of scrolling, for instance, or upon first load), you return the attributes from the cache that you've populated in prepareLayout
.
In conclusion to your question: In step 2, how and when can I obtain the cell size?
Answer: Each cell size is obtained within the prepareLayout
method of your custom UICollectionViewFlowLayout
, and is calculated early in the life cycle of your UICollectionView
.
In step 3, how and when can I notify the layout of the changes?
Note that the tutorial does not account for new cells to be added at runtime:
Note: As prepareLayout() is called whenever the collection view’s layout is invalidated, there are many situations in a typical implementation where you might need to recalculate attributes here. For example, the bounds of the UICollectionView might change – such as when the orientation changes – or items may be added or removed from the collection. These cases are out of scope for this tutorial, but it’s important to be aware of them in a non-trivial implementation.
Like he wrote, it's a non trivial implementation that you might need. There is, however, a trivial (very inefficient) implementation that you might adopt if your data set is small (or for testing purposes). When you need to invalidate the layout because of screen rotation or adding/removing cells, you can purge the cache in the custom UICollectionViewFlowLayout
to force prepareLayout
to reinitialize the layout attributes.
For instance, when you have to call reloadData
on the collectionView, also make a call to your custom layout class, to delete the cache:
cache.removeAll()
I realise this is not a complete answer, but some pointers regarding your steps 2 and 3 may be found in the subclassing notes for UICollectionViewLayout
.
I presume you have subclassed UICollectionViewFlowLayout
since off the top of my head I believe this is a good starting point for making adjustments to the layout to get the staggered appearance you want.
For step 2 layoutAttributesForElementsInRect(_:)
should provide the layout attributes for the self sized cells.
For step 3 your layout will have shouldInvalidateLayoutForPreferredLayoutAttributes(_:withOriginalAttributes:)
called with the changed cell sizes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With