Custom UICollectionView: Global Headers
16 Jun 2016
UICollectionView is an extremely capable UIKit component, which since its introduction in iOS6 has enabled a broad variety of attractive UI layouts.
The power of Collection View is in its remarkable flexibility, with totally custom layouts and sleek dynamic interactive transitions between them. That flexibility is also the main reason why proper handling of these aspects requires experience and sometimes could be way more complex compared to dealing with the rest of UIKit.
This article is about extending the concept of collection view section headers via adding a configurable, pinnable, and stretchable global header. That will allow building UI interfaces like the one shown here, which comes a part of the sample app. The sample app project relies on a simple, configurable custom layout that is available as an open source framework and can used in your project to enable the same kind of functionality. The layout works with both iOS8 and iOS9, and is optimized for performance via using invalidation contexts to rebuild only those parts of UI that actually changed during scrolling.
The build-in collection view Flow Layout has the notion of section headers and footers, and as of iOS9 also provides built-in support for floating section headers similar to those seen in table views. In addition to that, we want to be able to optionally add a global header that would always stay on top and be stretchable. When a global header is turned on, the other sections should be sticking to it when scrolling otherwise they should behave exactly as defined by the Flow Layout’s
sectionHeadersPinToVisibleBoundsproperty. The layout should work for both iOS8 and iOS9.
In general, a good way to get into custom layouts is to start with built-in Flow Layout. Given it covers a large range of line-oriented layouts with notion of rows and columns, tweaking
UICollectionViewFlowLayout is often the best and fastest way to achieve desired customizations. Apple strongly recommends this approach, and provides specific scenarios for subclassing.
That is clearly no exception for our case. Since the Flow Layout already comes with support for section headers and footers, all we need to do is to leverage it and to build on top.
Before diving into implementation, there are a few things to consider regarding the concept of global section header. Since collection view sections are data driven, shell we count on the data source to provide for our global section as well? Or perhaps shall it rather be treated as a static part of the layout, similar to decoration views?
There are obviously multiple possible approaches, including the one shown at a WWDC 2014 session: Advanced User Interfaces with Collection Views. The session introduced the idea of aggregate data sources, which among many other things include support for global sections.
After some thinking on the subject, my preference was towards keeping things simple and flexible. Our custom layout will rely on collection view datasource to provide the global section, and it will simply assume that it will be the first section there. This way there is no need for dedicated hierarchy of data sources, and it will allow our custom layout to be useful in a smart way without unnecessarily bloating things up.
Custom Layout Attributes
Since the requirements include section header stretching, our custom layout will have to manipulate the headers’ frames. The dynamic height changes should be propagated back to collection view items, so that they can adjust their UI in corresponding
applyLayoutAttributes: methods. In order to enable such communication, let’s subclass
UICollectionViewLayoutAttributes and define the
As collection view copies layout attribute objects, we also have to conform to the NSCopying protocol and implement its methods.
Section headers in a rect
The central place for a custom layout implementation is typically in the
layoutAttributesForElementsInRect: function, which returns an array of layout attributes containing a layout attribute for each cell, supplementary, or decoration view that should be displayed in the passed rectangle.
Since we’re going to move forward with our implementation via subclassing
UICollectionViewFlowLayout, most of the key ingredients there should already be provided to us out of the box.
The main idea is to simply add our custom sections’ layout attributes to those already handled by the Flow Layout.
Before we do that, let’s first write a few helper functions that will help us deal with collection view sections.
First, let’s calculate the indexes of all sections confined in a rect. That should include both regular sections and the custom sections, i.e the global header and the headers that are currently “sticking” to it:
The code above builds a set of all section indexes, via going through layout attributes and searching for elements that are:
- regular sections
- cells that are in a section with custom header
The sections indexes of all matching elements are added to the set of unique indexes. In case of the global section, we always want to have it so it is added there as well.
Now it’s trivial to calculate the indexes of only the custom sections in a rect, i.e. excluding the regular headers provided by
Layout Attributes headers in a rect
Now we are ready to write our
layoutAttributesForElementsInRect(:) function as the following:
The code above adds our custom sections attributes to the array of regular layout attributes, and then adjusts those so that the global section is always top and all other sections behave according to the requirements.
adjustLayoutAttributes: function is a bit on the lengthy side though still relatively straightforward:
Basically, the function first establishes the section boundaries of (minY, maxY) and then based on that configures
sectionFrame.origin.y so it intelligently follows the collection view content offset.
If stretching a header, the section frame height is set accordingly and the stretch information is recorded in our custom layout attributes
Finally, we also need to manage sections’
zIndex, so that the global header and sticky headers are always on top.
Custom Layout Invalidation
The key to building high-performance layout is to recompute only those parts that actually changed. This way when a user scrolls, we will not end up repeatedly calling computationally intensive
layoutAttributesForElementsInRect: but instead whenever possible just be calling
layoutAttributesForSupplementaryViewOfKind directly. According to Apple’s documentation:
an invalidation context lets you specify which parts of the layout changed. To define a custom invalidation context for your layout, subclass the UICollectionViewLayoutInvalidationContext class. In your subclass, define custom properties that represent the parts of your layout data that can be recomputed independently.
For our case the Flow Layout is already using its invalidation context for optimized layout updates. Therefore, instead of creating our own custom invalidation context class it is sufficient to simply plug into the existing invalidation process.
Since we are handling the layout for collection view sections, we should also take care of invalidating those that are affected by the bounds changes:
Just one more thing
At that point, we are almost done! However the requirements mention
sectionHeadersPinToVisibleBounds, which is a boolean property of
UICollectionViewFlowLayout that enables out-of-the-box sticky headers in iOS9. Since we are now explicitly managing the sections headers, we also need to make sure there is no interference with the built-in implementation. The easiest way around that might be doing something along of lines of:
The problem is that the
sectionHeadersPinToVisibleBounds property is not available in iOS8, and so far there seems to be no reasonable way to use Swift property observers with conditional compilation. Luckily, we can still fall back to using KVO:
This way our custom layout will work both for iOS8 and iOS9, and there will be no collision with the iOS9 built-in sticky headers functionality.
The article went through major steps of implementing a custom collection view flow layout, extending the concept of sections headers according to specific requirements.