In the previous part of this tutorial, basic usage of UICollectionViewFlowLayout was discussed.
UICollectionViewFlowLayout is a powerful and convenient way to create beautiful collections, but sometimes a more sophisticated approach is needed. This tutorial will show, how to subclass UICollectionViewLayout to achieve more unique layouts.
The demo app
We are going to build a gridview with some beautiful nature pictures. It can be used as a category view for a gallery or a photo app.
We’ll grab an existing project from the part I of this tutorial and change it to look like this:
You can find the complete code here (tested with Xcode 8.1, iOS 10.1):
As a basis, we will use the project created in the previous part of this tutorial.
You can grab it from the Github repository.
Or if you wish to learn the basics of the UICollectionView, you’d better start with the first part.
Add a new empty User Interface file (XIB) and name it “GalleryItemCommentView”.
Navigate to the storyboard and copy “Gallery Item Comment View” from the collectionview. Paste it to recently created XIB “GalleryItemCommentView”.
After that, open ViewController class and add following lines to the viewDidLoad method before reloading the collectionview.
The reason for doing it is that supplementary views are not supported in the storyboard for custom layouts. Therefore, supplementary views have to be designed in xibs and registered separately.
Then, add a new class “GalleryItemsLayout”, which extends “UICollectionViewLayout”.
Now open the main storyboard and select the collection view.
Navigate to Attributes inspector and change Collection View’s Layout to Custom. After that, an additional text field “Class” appears. Enter “GalleryItemsLayout” into this text field.
Add the following properties to GalleryItemsLayout header:
Open again the storyboard and navigate to the Identity Inspector of the Gallery Items Layout:
Define following values under User Defined Runtime Attributes:
horizontalInset -> 15
verticalInset -> 15
minimumItemWidth -> 150
maximumItemWidth -> 300
itemHeight -> 250
Also select the ImageView inside the GalleryItemCollectionViewCell, open the Attributes Inspector, change image scaling mode to “Aspect Fill” and check the “Clip subviews” checkbox.
In order to create custom collection view layouts, one must subclass UICollectionViewLayout and implement following methods:
This method should calculate and return the total size of all content (not just visible). This information will be used to configure scrolling, so if you wish to support both horizontal and vertical scrolling, both contentWidth and contentHeight must exceed collectionview’s frame size.
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;
This method should return layout attributes for the requested indexPath. Layout attributes will be precalculated in the prepareLayout method.
Preparelayout is called whenever the collectionview’s layout is invalidated. Additionally, it is called before the collectionview is being laid out for the very first time.
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;
This method should return layout attributes for all possible items within the defined rect, including supplementary views. It is used to perform layout in an as-needed-on-screen fashion.
Additionally, there are a couple of methods that are not required, but might be useful to implement:
Returning YES here will call prepareLayout every time, when the collectionview is scrolled, so it is not performance wise. Returning always NO, will stop invalidating the collectionview even when invalidateLayout is explicitly called. However, implementing custom logic here to check, whether invalidation should be done or not, might be useful.
- (UICollectionViewLayoutAttributes *)layoutAttributesForDecorationViewOfKind:(NSString*)elementKind atIndexPath:(NSIndexPath *)indexPath;
This method must be implemented, if the layout supports any supplementary view types. Its behaviour is similar to the layoutAttributesForItemAtIndexPath method.
Our demo app doesn’t have too much data, so all main calculations will be done in the prepareLayout method. Preparelayout is a really convenient place to pre-calculate all layout options.
Firstly, add an additional instance variable called _layoutAttributes. It will hold all calculated layout data.
Additionally, add a variable called _contentSize. Because our layout will be partially random, it is impossible to calculate contentSize deterministically without passing all layout attributes. Thus, we will calculate contentSize in the prepareLayout method, where all layout attributes will be checked anyway.
Then, let’s implement the prepareLayout method:
What’s done here?
- _layoutAttributes is the dictionary to save all calculations for every view. Initialise (or reinitialise) it before updating layout calculations.
- The very first view in our layout is the header view. When initialising UICollectionViewLayoutAttributes, different methods must be used for cells and supplementary views. Here, we define that the header view will be at the very top and 1/4 of the height of the cell.
- By using handy methods numberOfSections and numberOfItemsInSection we find out, how many sections and how many items in every section are there.
- Then we create empty layout attributes for the current indexPath.
- The layout uses random cell width sizes, so to find out the correct frame for the item, we must know, whether there is enough space for 2 or more cells or not. If yes, we take the random size.
- Otherwise, the cell will just fill the remaining space.
- Lastly, the frame is saved in the layout attributes.
- xOffset is increased by cell width to know the x origin of the next cell.
- If there is no enough space for one more cell, we must start from the next row. The only exception is the very last line.
- Add additional vertical space for the last line
- Save the resulting collection view contentSize.
Random size calculation is implemented like this:
And layout keys defined in two simple methods:
The last thing (and the easiest) is to return correct layout attributes in correct methods.
- In collectionViewContentSize we return the size previously calculated in the prepareLayout method.
- In layoutAttributesForSupplementaryViewOfKind: and layoutAttributesForItemAtIndexPath: corresponding attributes from the dictionary is found and returned.
- The tricky part is implementation of the layoutAttributesForElementsInRect: method. To find attributes for the specified rect, we must filter the dictionary to find only those attributes, which intersect with the request rect. One of the possible solutions is to use filteredArrayUsingPredicate: to filter out appropriate NSDictionary keys and then return only those NSDictionary objects that match the keys.
Finally, we need to implement the shouldInvalidateLayoutForBoundsChange: method. The layout must be recalculated on the device orientation change, but not harm the performance. Thus, the layout needs to be redrawn only when the collectionview rect gets actually changed:
And that’s it! Using those basic techniques, you can create really amazing custom layouts.
You can find the complete code here (tested with Xcode 8.1, iOS 10.1):