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:
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:
Create a UICollectionViewController subclass and assign it to the controller component from the storyboard:
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:
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) ( <NSLayoutConstraint:0x7fc15bc2be20 H:[UILabel:0x7fc15bd93c00'Lorem ipsum dolor sit ame...'(<=182.5)]>, <NSLayoutConstraint:0x7fc15bc2ad10 H:[UILabel:0x7fc15bd93c00'Lorem ipsum dolor sit ame...']-(0)-| (Names: '|':UIView:0x7fc15bd938f0 )>, <NSLayoutConstraint:0x7fc15bc2ad60 H:|-(0)-[UILabel:0x7fc15bd93c00'Lorem ipsum dolor sit ame...'] (Names: '|':UIView:0x7fc15bd938f0 )>, <NSAutoresizingMaskLayoutConstraint:0x7fc15bd87550 h=--& v=--& H:[UIView:0x7fc15bd938f0(320)]> ) Will attempt to recover by breaking constraint <NSLayoutConstraint:0x7fc15bc2be20 H:[UILabel:0x7fc15bd93c00'Lorem ipsum dolor sit ame...'(<=182.5)]> Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.` |
Drag a UILabel component from the Object Library to the cell view and create an outlet in class extension:
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:
1 2 3 4 5 6 7 8 9 |
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { CCRCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath]; // Configure the cell cell.text = self.textItems[indexPath.row]; return cell; } |
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:
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (void)viewDidLoad { [super viewDidLoad]; // Register cell classes [self.collectionView registerNib:[UINib nibWithNibName:@"CCRCollectionViewCell" bundle:[NSBundle mainBundle]] forCellWithReuseIdentifier:reuseIdentifier]; // Set the estimated width of the cells to half the screen width (2 columns layout) UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionViewLayout; CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds); layout.estimatedItemSize = CGSizeMake(screenWidth / 2, 88.0); } |
If we run the application now, it crashes with this error message:
1 |
The behavior of the UICollectionViewFlowLayout is not defined because the item width must be less than the width of the UICollectionView minus the section insets left and right values. |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (void)awakeFromNib { [super awakeFromNib]; self.label.translatesAutoresizingMaskIntoConstraints = NO; CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds); // The cell width is half the screen width minus the gap between the cells // The gap should be slightly larger than the minium space between cells set for the flow layout to prevent layout and scrolling issues CGFloat cellWidth = (screenWidth - 10) / 2; [self.label addConstraint:[NSLayoutConstraint constraintWithItem:self.label attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationLessThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:cellWidth]]; } |
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.