Collection view with self-sizing cells

This article is the logical continuation of the blog post I’ve written last week about the self-sizing cell in tableviews. This time we will take a close look at the auto-adapting cells in collections views.

The sample project is available for download on GitHub.

Before going further, you should know that self-sizing cells are supported starting from iOS8. If you have to implement something similar in the previous iOS versions, you have to calculate the cells sizes and set their frame in your own UICollectionViewFlowLayout subclass.

The collection view from the sample project only supports portrait orientation with two columns layout:

collection view screenshot

The cells have fixed width and automatically resize their height to fit the content.

The rotation is disabled because it generates a flow layout error; support for device rotation is beyond the scope of this article.

As for the table view article, I describe step by step the collection view implementation.

Set up the collection view

Open XCode and create a Single View Application. Delete the default view controller from the Main.storyboard, as well as the ViewController.h and ViewController.m files from the project file tree. We will replace them with a custom collection view controller.

Drag a collection view controller component from the Object Library to the storyboard and set it as the Initial View Controller:

collection view controller in the storyboard

Create a UICollectionViewController subclass and assign it to the controller component from the storyboard:

custom collection view controller subclass

Delete the commented code automatically generated in controller .m file and keep only the methods -viewDidLoad, -numberOfSectionsInCollectionView:, -collectionView:numberOfItemsInSection: and -collectionView:cellForItemAtIndexPath. The last three of them are the data source methods automatically invoked by the collection view to populate itself with data.

To keep things simple, the data source will be an array of variable length strings.

All collection view cells are displayed in the same section. The -numberOfSectionsInCollectionView: returns 1, and the collectionView:numberOfItemsInSection: returns the number of strings in the data source array.

Configure the collection view cell template

UICollectionViewCell is rarely used directly as the template cell class for a custom collection view. A custom subclass is usually created to display the content in some specific way.

In our case, the content is plain text and it is displayed in a multiline UILabel. We create a UICollectionViewCell subclass and its XIB file, then we associate the XIB to the custom class:

collection view cell with XIB

The size of the cell view in the XIB is 50 x 50 points, which is the default size of the collection view cells as set in the flow layout. Even if it’s a bit hard to work with a cell this small in Interface Builder, it’s better to not change the default size. The problem is that Auto Layout considers the manually set size as being fixed and generates a NSAutoresizingMaskLayoutConstraint error when it tries to adjust the cells height automatically:

Drag a UILabel component from the Object Library to the cell view and create an outlet in class extension:

collection view cell UILabel outlet

Set the Lines property of the label to 0 in Attributes inspector; this enables the label to display multiple lines of text.

Add a NSString property to the custom cell class interface to be able to set the cell content from the data source. In the setter, update the text property of the UILabel.

In the -viewDidLoad method of the view controller, register the custom cell class instead of the standard UICollectionViewCell.

Import the custom cell header file and change the -collectionView:cellForItemAtIndexPath method as follows:

We’re almost there.
Build and run the project. You should see the collection view displaying the cells according the default layout settings. All cells have a fixed size of 50 x 50 and are evenly spaced on the screen. The text is truncated at the bounds of each cell.

Resizable cells

There are several ways to automatically resize the collection view cells to fit their content. They involve more or less code, depending on how much control the developer wants to have on the layout process.
Here I’ll present the easiest way to enable self-sizing cells with very few modifications to the existing code; in return, I give away almost all responsibility about how the cells are displayed in the collection view and the results can become pretty unpredictable if Auto Layout doesn’t like the size and constraint suggestions I make.

This method uses Auto Layout and the UICollectionViewFlowLayout instance property estimatedItemSize to calculate the content size of each cell and set its frame in the layout attributes.

First, open the cell XIB file and create the constraints between the label and each side of the cell:

auto layout constraints on collection view cell

Then, in the view controller class, set the estimatedItemSize property in the -viewDidLoad: method. The default value of this property is CGSizeZero; theoretically, we could set this property to any size different from zero and the collection view layout should automatically use the size provided by Auto Layout to set the cell frame. I’ve tested with several different values for the width, but the one which worked best in this case is half the screen size, which is expected for a two-column layout.

If we run the application now, it crashes with this error message:

Even though we set the constraints between the label and the cell that contains it, and the label multiline display is enabled, the collection view layout is not able to figure out yet the right height for the cell. Instead, it tries to increase the its width to fit the text. But the width exceeds the collection view width (and, for that matter, the screen width), giving no option to the system but to crash the app.

The solution is to add another Auto Layout constraint to the label to keep its width within the bounds of the screen.
In the -awakeFromNib method of the collection view cell subclass, create a constraint on the width property of the label so it doesn’t exceed half the screen width minus a value just a bit larger than the maximum inter-cell spacing set by the flow layout (which is 10 points by default, but I modified it to 5 points to reduce the gutter between the columns).
If we subtract the exact minimum spacing value from the screen width, the collection view contentSize is not calculated correctly and we run into problems like the collection view not scrolling or the cells overlapping.

If we run the application again, it should display the cells correctly in two columns, with the text fully visible in each cell.

Conclusion

After watching the WWDC 2014 session 226, I thought that implementing self sizing cells in collection view would be straightforward and I hesitated to write a blog post on the topic. But when I dove into the code, I run into several problems that I didn’t expect. I hope the solutions I figured out will help someone in the future.

 

Catalin Rosioru