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
How to Check If iOS App Is in Background
How to Stop Firebase from Logging Status Updates When App Is Launched
Does Firebase Cloud Messaging Support Voip Pushkit Services
Pod Error in Xcode "Id: Framework Not Found Pods"
Making Video from Uiimage Array with Different Transition Animations
Swift - Uiimagepickercontroller - How to Use It
How to Stop Uitableview from Clipping Uitableviewcell Contents in iOS 7
Nsurlconnection Deprecated in iOS9
iOS Tab Bar Icons Keep Getting Larger
How to Change the Size of Uiactivityindicator
How to Populate Uitableview from the Bottom Upwards
iPhone Collecting Coremotion Data in the Background. (Longer Than 10 Mins)
Are Afnetworking Success/Failure Blocks Invoked on the Main Thread