Uicollectionview Sticky Header in Swift

UICollectionView sticky header in swift

The final solution I found:

Using this custom flow layout it was possible to fix this sticky header:

class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout {

override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {

var superAttributes: [UICollectionViewLayoutAttributes]? = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]

if superAttributes == nil {
// If superAttributes couldn't cast, return
return super.layoutAttributesForElementsInRect(rect)
}

let contentOffset = collectionView!.contentOffset
var missingSections = NSMutableIndexSet()

for layoutAttributes in superAttributes! {
if (layoutAttributes.representedElementCategory == .Cell) {
if let indexPath = layoutAttributes.indexPath {
missingSections.addIndex(layoutAttributes.indexPath.section)
}
}
}

for layoutAttributes in superAttributes! {
if let representedElementKind = layoutAttributes.representedElementKind {
if representedElementKind == UICollectionElementKindSectionHeader {
if let indexPath = layoutAttributes.indexPath {
missingSections.removeIndex(indexPath.section)
}
}
}
}

missingSections.enumerateIndexesUsingBlock { idx, stop in
let indexPath = NSIndexPath(forItem: 0, inSection: idx)
if let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath) {
superAttributes!.append(layoutAttributes)
}
}

for layoutAttributes in superAttributes! {
if let representedElementKind = layoutAttributes.representedElementKind {
if representedElementKind == UICollectionElementKindSectionHeader {
let section = layoutAttributes.indexPath!.section
let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)

let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)!
let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)!

let (firstCellAttributes: UICollectionViewLayoutAttributes, lastCellAttributes: UICollectionViewLayoutAttributes) = {
if (self.collectionView!.numberOfItemsInSection(section) > 0) {
return (
self.layoutAttributesForItemAtIndexPath(firstCellIndexPath),
self.layoutAttributesForItemAtIndexPath(lastCellIndexPath))
} else {
return (
self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath),
self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath))
}
}()

let headerHeight = CGRectGetHeight(layoutAttributes.frame)
var origin = layoutAttributes.frame.origin

origin.y = min(contentOffset.y, (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
// Uncomment this line for normal behaviour:
// origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))

layoutAttributes.zIndex = 1024
layoutAttributes.frame = CGRect(origin: origin, size: layoutAttributes.frame.size)
}
}
}

return superAttributes
}

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}

}

To create a layout where the headers are sticky like traditional, change this line:

origin.y = min(contentOffset.y, (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))

to this line:

origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))

Hoping this is useful for others!

Update

Updated to fix a crash (thanks to Robert Atkins!) and some updates to Swift 1.2

tvOS & iOS 9

tvOS and iOS 9 introduced the property sectionHeadersPinToVisibleBounds which can be used

UICollectionView sticky header for specific section in Swift

This caused the header's layout attribute to not be included in the attributes array when you iterated over in the for loop, resulting in the layout position no longer being adjusted to its "sticky" position at the top of the screen.

Adding these lines right before the for loop to add the sticky header's layout attributes to the attributes array if they are not there:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

var layoutAttributes = [UICollectionViewLayoutAttributes]()

guard let cellLayoutAttributesInRect = super.layoutAttributesForElements(in: rect) else { return nil }

// add the sticky header's layout attribute to the attributes array if they are not there
if let stickyHeaderIndexPath = stickyHeaderIndexPath,
let stickyAttribute = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: stickyHeaderIndexPath),
!layoutAttributes.contains(stickyAttribute) {
layoutAttributes.append(stickyAttribute)
}

return layoutAttributes
}

UICollectionView with a sticky header

Fix by Todd Laney to handle Horizontal and Vertical scrolling and to take into account the sectionInsets:

https://gist.github.com/evadne/4544569

@implementation StickyHeaderFlowLayout

- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {

NSMutableArray *answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];

NSMutableIndexSet *missingSections = [NSMutableIndexSet indexSet];
for (NSUInteger idx=0; idx<[answer count]; idx++) {
UICollectionViewLayoutAttributes *layoutAttributes = answer[idx];

if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
[missingSections addIndex:layoutAttributes.indexPath.section]; // remember that we need to layout header for this section
}
if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
[answer removeObjectAtIndex:idx]; // remove layout of header done by our super, we will do it right later
idx--;
}
}

// layout all headers needed for the rect using self code
[missingSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
if (layoutAttributes != nil) {
[answer addObject:layoutAttributes];
}
}];

return answer;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
UICollectionView * const cv = self.collectionView;
CGPoint const contentOffset = cv.contentOffset;
CGPoint nextHeaderOrigin = CGPointMake(INFINITY, INFINITY);

if (indexPath.section+1 < [cv numberOfSections]) {
UICollectionViewLayoutAttributes *nextHeaderAttributes = [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:[NSIndexPath indexPathForItem:0 inSection:indexPath.section+1]];
nextHeaderOrigin = nextHeaderAttributes.frame.origin;
}

CGRect frame = attributes.frame;
if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
frame.origin.y = MIN(MAX(contentOffset.y, frame.origin.y), nextHeaderOrigin.y - CGRectGetHeight(frame));
}
else { // UICollectionViewScrollDirectionHorizontal
frame.origin.x = MIN(MAX(contentOffset.x, frame.origin.x), nextHeaderOrigin.x - CGRectGetWidth(frame));
}
attributes.zIndex = 1024;
attributes.frame = frame;
}
return attributes;
}

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
return attributes;
}
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
return attributes;
}

- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound {
return YES;
}

@end

How to custom header view that sticky in top screen when scroll UICollectionView?

UICollectionView is a subclass of UIScrollView. This means that is you assign a delegate to it then this delegate may listen to scrollViewDidScroll(_ scrollView: UIScrollView) method which will be called every time the offset changes in your collection view.

On this event you will need to get all of your header views which I assume you may get by calling visibleSupplementaryViews on your collection view and check its class type.

If the received view is indeed your header you will need to check its frame in comparison to your collection view and see if its position is on top. To do so you may convert frame to your coordinates:

func isHeader(headerView: UIView, onTopOfCollectionView collectionView: UICollectionView) -> Bool {
guard let collectionSuperView = collectionView.superview else {
return false // Collection view is not in view hierarchy. This will most likely never happen
}
let convertedFrame = headerView.convert(headerView.bounds, to: collectionSuperView) // Convert frame of the view to whatever is the superview of collection view
return convertedFrame.origin.y <= collectionView.frame.origin.y // Compare frame origins
}

So I guess the whole thing would be something like:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.visibleSupplementaryViews(<#My identifier here#>).forEach { view in
if let header = view as? MyHeaderView {
header.hideSecondaryViews = isHeader(headerView: header, onTopOfCollectionView: collectionView)
}
}
}

I hope this gets you on the right track.

Sticky UICollectionView Header F#

This can be done using the following code:

let layout = x.CollectionView.CollectionViewLayout :?> UICollectionViewFlowLayout
layout.SectionHeadersPinToVisibleBounds <- true

Adapted from UICollectionView sticky header in swift

In order to eliminate white space on top of collectionView do the following:

x.AutomaticallyAdjustsScrollViewInsets <- false
x.CollectionView.ContentInset <- UIEdgeInsets(Conversions.nfloat(-44),Conversions.nfloat(0), Conversions.nfloat(0), Conversions.nfloat(0))

Note that the value of -44 is used for iPhone X specifically, and might differ for other models.

Conversions is used to convert to nfloat. This is the code that does this conversion:

let inline nfloat (x:^a) : ^b = ((^a or ^b) : (static member op_Implicit : ^a -> nfloat) x)

Finally, if you wish to eliminate vertical bouncing on top of the UICollectionView add the following code to your UICollectionViewController :

override x.Scrolled(scrollView : UIScrollView) = 
if (scrollView.ContentOffset.Y < Conversions.nfloat(0)) then
scrollView.ContentOffset <- CGPoint(scrollView.ContentOffset.X, Conversions.nfloat(0))


Related Topics



Leave a reply



Submit