So I have implemented working sticky headers in my UICollectionView in part by returning YES
from shouldInvalidateLayoutForBoundsChange:
. However, this impacts performance and I do not want to invalidate the entire layout, only my header section.
Now, according to the official documentation I can use UICollectionViewLayoutInvalidationContext
to define a custom invalidation context for my layout, but the documentation is very lacking. It asks me to "define custom properties that represent the parts of your layout data that can be recomputed independently", but I don't understand what they mean by this.
Has anyone got any experience subclassing UICollectionViewLayoutInvalidationContext
?
This is for iOS8
I experimented a bit and I think I figured out the clean way to use the invalidation layout, at least until Apple expands on the documentation a bit.
The problem I was trying to solve was getting sticky headers in the collection view. I had working code for this using the subclass of FlowLayout and overriding layoutAttributesForElementsInRect: (you can find working examples on google). This required me to always return true from shouldInvalidateLayoutForBoundsChange: which is the supposed major performance kick in the nuts that Apple wants us to avoid with contextual invalidation.
The Clean Context Invalidation
You only need to subclass the UICollectionViewFlowLayout. I didn't need a subclass for UICollectionViewLayoutInvalidationContext, but then this might be a pretty straightforward use case.
As the collection view scrolls, the flow layout will start receiving shouldInvalidateLayoutForBoundsChange: calls. Since flow layout can already handle this, we'll return the superclass' answer at the end of the function. With simple scrolling this will be false, and will not re-layout the elements. But we need to re-layout the headers and have them stay at the top of the screen, so we'll tell the collection view to invalidate only the context that we'll provide:
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
invalidateLayoutWithContext(invalidationContextForBoundsChange(newBounds))
return super.shouldInvalidateLayoutForBoundsChange(newBounds)
}
This means we need to override the invalidationContextForBoundsChange: function too. Since the internal workings of this function are unknown, we'll just ask the superclass for the invalidation context object, determine which collection view elements we want to invalidate, and add those elements to the invalidation context. I took some of the code out to focus on the essentials here:
override func invalidationContextForBoundsChange(newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext! {
var context = super.invalidationContextForBoundsChange(newBounds)
if /... we find a header in newBounds that needs to be invalidated .../ {
context.invalidateSupplementaryElementsOfKind(UICollectionElementKindSectionHeader, atIndexPaths:[NSIndexPath(forItem: 0, inSection:headerIndexPath.section)] )
}
return context
}
That's it. The header and nothing but the header is invalidated. The flow layout will receive only one call to layoutAttributesForSupplementaryViewOfKind: with the indexPath in the invalidation context. If you needed to invalidate cells or decorators, there are other invalidate* functions on the UICollectionViewLayoutInvalidationContext.
The hardest part really is determining the indexPaths of the headers in the invalidationContextForBoundsChange: function. Both my headers and cells are dynamically sized and it took some acrobatics to get it to work from just looking at the bounds CGRect, since the most obviously helpful function, indexPathForItemAtPoint:, returns nothing if the point is on a header, footer, decorator or row spacing.
As for the performance, I didn't do a full measurement, but a quick glance at Time Profiler while scrolling shows that it's doing something right (the smaller spike on the right is while scrolling).