Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to develop a custom UICollectionViewLayout that has staggered columns with self-sizing cells?

Tags:

ios

swift

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:

Staggered 2 column layout on Android

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.

enter image description here

Step 2. Then the cells are created and sized with autolayout.

enter image description here

Step 3. Then the controller notifies the of the cell sizes so the layout is updated.

enter image description here

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?

like image 431
eliocs Avatar asked Sep 04 '15 12:09

eliocs


2 Answers

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()
like image 123
Kelvin Lau Avatar answered Oct 05 '22 23:10

Kelvin Lau


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.

like image 45
Seán Labastille Avatar answered Oct 05 '22 23:10

Seán Labastille