I'm trying to implement a single sticky header in a UICollectionView.
My sticky header behavior is a bit different than the usual one you can see e.g. in UITableView. I have 3 headers in the collection view and I want only one of them to be sticky and stick to the top when the content is scrolled.
My code works pretty well. However, when I scroll down, the sticky header disappears suddenly at some point. Scrolling back makes the header appear again. What am I doing wrong?
I am attaching a implementation of my custom layout. It's a subclass of UICollectionViewFlowLayout.
#implementation CustomFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *attributes = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
CGPoint const contentOffset = self.collectionView.contentOffset;
for (UICollectionViewLayoutAttributes *layoutAttributes in attributes)
{
// Adjust the sticky header frame.
if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader] &&
layoutAttributes.indexPath.section == SectionWithStickyHeadeIndex)
{
NSInteger numberOfItemsInSection = [self.collectionView numberOfItemsInSection:SectionWithStickyHeadeIndex];
NSIndexPath *firstObjectIndexPath = [NSIndexPath indexPathForItem:0
inSection:SectionWithStickyHeadeIndex];
UICollectionViewLayoutAttributes *firstObjectAttrs;
if (numberOfItemsInSection > 0)
{
firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
}
else
{
firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
atIndexPath:firstObjectIndexPath];
}
CGPoint origin = layoutAttributes.frame.origin;
// Adjust the header origin so it sticks to the top.
origin.y = MAX(contentOffset.y + self.collectionView.contentInset.top,
CGRectGetMinY(firstObjectAttrs.frame) - CGRectGetHeight(layoutAttributes.frame));
layoutAttributes.zIndex = CGFLOAT_MAX;
layoutAttributes.frame = (CGRect)
{
.origin = origin,
.size = layoutAttributes.frame.size
};
break;
}
}
return attributes;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBound
{
return YES;
}
#end
I'm not 100% sure on this, but it looks like once you scrolled down far enough, the header's original position was no longer located inside the rect argument. This caused the header's layout attributes to not be included in the attributes array you iterated over in in the for loop, resulting in the layout position no longer being adjusted to its "sticky" position at the top of the screen.
Try adding these lines right before the for loop to add the sticky header's layout attributes to the attributes array if they are not already there:
NSIndexPath *stickyHeaderIndexPath = [NSIndexPath indexPathForItem:0 inSection:SectionWithStickyHeaderIndex];
UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
atIndexPath:stickyHeaderIndexPath];
if (![attributes containsObject:layoutAttributes])
{
[attributes addObject:layoutAttributes];
}
The answer in Swift 5
let headerIndexPath = IndexPath(item: 0, section: 0)
if let headerAttributes = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: headerIndexPath) {
if !attributes.contains(headerAttributes) {
attributes.append(headerAttributes)
}
}
All thanks to #BrendanCain
Related
I have a UICollectionView which expands on clicking a cell and once the screen fills it becomes scrollable.
Now when I scroll down I need my header view to scroll down with it and for that I've implemented the logic in the layoutAttributesForSupplementaryViewOfKind method in my custom UICollectionViewLayout class.
This works fine but now the issue is that when I the content becomes scrollable and I scroll down few cells and immediately click on a cell to shrink the content back to one screen at that point the header view doesn't gets arranged, i.e it still remains in the last scrolled position.
But there after if I perform any other action like cell tap it gets arranged properly.
I've tried calling setNeedsLayout, setNeedsDisplay and layoutSubviews where I reload my UICollectionView but the header still doesn't updates to its proper position.
Below is the code for my layoutAttributesForSupplementaryViewOfKind method.
Any help is appreciated.
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
if (![kind isEqualToString:[myGridHeaderView kind]]) {
return nil;
}
myGridHeaderPosition headerPosition = [[self collectionView] headerPositionAtIndexPath:indexPath];
CGRect cellRect = [[self delegate] getRectForHeaderAtIndex:indexPath headerPosition:headerPosition];
if (CGRectEqualToRect(cellRect, CGRectZero)) {
return nil;
}
myGridHeaderLayoutAttribute* attributes = [myGridHeaderLayoutAttribute layoutAttributesForSupplementaryViewOfKind:kind withIndexPath:indexPath];
CGPoint centerPoint = CGPointMake(CGRectGetMidX(cellRect), CGRectGetMidY(cellRect));
CGSize size = cellRect.size;
UICollectionView * const cv = self.collectionView;
NSInteger zIndex = 1;
CGPoint const contentOffset = cv.contentOffset;
if (contentOffset.x > 0)
{
if (headerPosition != myGridHeaderPositionColumn)
{
centerPoint.x += contentOffset.x;
}
zIndex = 1005;
}
if (contentOffset.y > 0)
{
if (headerPosition != myGridHeaderPositionRow)
{
centerPoint.y += contentOffset.y;
}
zIndex = 1005;
}
if (headerPosition == myGridHeaderPositionCommon) {
zIndex = 1024;
}
attributes.zIndex = zIndex;
attributes.headerPosition = headerPosition;
attributes.center = centerPoint;
attributes.size = size;
attributes.alpha = 1.0;
return attributes;
}
When you scroll up and down , header will be visible and hidden , for use this code.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
isScrollingStart=YES;
NSLog(#"scrollViewDidScroll %f , %f",scrollView.contentOffset.x,scrollView.contentOffset.y);
if (scrollView.contentOffset.y<=124) {
_img_top_header.alpha=scrollView.contentOffset.y/124;
}
else
{
_img_top_header.alpha=1.0;
}
}
must be set image in header.
I have a problem managing my UICollectionsView with flowlayout and orientation. Basically, the problems happens when I switch to landscape, some of the UICollectionViewCells are missing. They reappear once I start scrolling. It should be able to show all the cell since there is so much more room on the contentSize.
I even tried in my layout:
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
With the same results.
Anyone have any ideas on this problem? I included a image on how my layout is organized and the expected behavior:
To add more details, each of the color coded cells belongs to its own section and I am shifting my 2nd section to the right on orientation change using:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
NSIndexPath* indexPath = attributes.indexPath;
if (nil == attributes.representedElementKind) {
attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
} else {
NSString *kind = attributes.representedElementKind;
attributes.frame = [self layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath].frame;
}
}
return attributesToReturn;
}
It seems the number of items in the layoutAttributesForElementsInRect are completely different in landscape and in portrait.
Finally figure it out. I increased my the rect to a large size when it's on landscape on:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
CGRect *newRect = CGRectMake(0,0, screensize.width, screensize.height);
NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:newRect];
...
}
I am trying to customise the positions of the headers in a UICollectionView using a subclassed UICollectionViewFlowLayout class (based loosely on the code for stacked headers which is shown enter link description here).
As a minimal test, let's say I just want to add a fixed offset to the position of all headers:
I add all headers to the array returned by layoutAttributesForElementsInRect so that all are always processed (this may be the cause of the problem, I'm not sure)
I then update each header by adding a fixed offset in layoutAttributesForSupplementaryViewOfKind
The full implementation is included at the end of this post.
(By the way, I know that adding all headers, including those outside the rect, is not strictly speaking necessary in the first step, but this is a simplified example of a more complex customisation in position I want to make which would cause all headers to be displayed in the draw rect.)
However, when I run the code I get the following NSInternalInconsistencyException:
2014-01-15 00:41:50.130 CollectionStackedHeaders[60777:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'layout attributes for supplementary item at index path (<NSIndexPath: 0x8a7db90> {length = 2, path = 0 - 0})
changed from <UICollectionViewLayoutAttributes: 0x8a7f8b0> index path: (<NSIndexPath: 0x8a7d9c0> {length = 2, path = 0 - 0}); element kind: (UICollectionElementKindSectionHeader); frame = (0 0; 320 50);
to <UICollectionViewLayoutAttributes: 0x8a7fb80> index path: (<NSIndexPath: 0x8a7db90> {length = 2, path = 0 - 0}); element kind: (UICollectionElementKindSectionHeader); frame = (0 50; 320 50); zIndex = 1024;
without invalidating the layout'
It seems that this is caused by the update of the attributes, as if I comment out the following two lines it works fine:
attributes.zIndex = 1024;
attributes.frame = frame;
What is causing this error, and what can I do to get my simple example up and running?
Here is the full class implementation for this simple example:
#implementation myStackedHeaderFlowLayout
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
// Call super to get elements
NSMutableArray* answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
// As a test, always add first header to the answer array
NSArray* indexes = [NSArray arrayWithObjects: [NSNumber numberWithInt:0], nil];
for (NSNumber* sectionNumber in indexes) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:[sectionNumber integerValue]];
UICollectionViewLayoutAttributes* layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
if (layoutAttributes) {
[answer removeObject:layoutAttributes]; // remove if already present
[answer addObject:layoutAttributes];
}
}
return answer;
}
- (UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryViewOfKind:(NSString*)kind atIndexPath:(NSIndexPath*)indexPath {
// Call super to get base attributes
UICollectionViewLayoutAttributes* attributes = [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
CGRect frame = attributes.frame;
frame.origin.y += 50;
// Update attributes position here - causes the problem
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
layout attributes for supplementary item at index path (<NSIndexPath>)
changed from <UICollectionViewLayoutAttributes>
to <UICollectionViewLayoutAttributes>
without invalidating the layout
In my experience, the NSInternalInconsistencyException with the description above is thrown when the array returned from layoutAttributesForElementsInRect: contains two UICollectionViewLayoutAttributes objects with the same index path and (supplementary) element category.
You're receiving this error because you're adjusting the frame from (0 0; 320 50) to (0 50; 320 50) without re-validating the layout (likely you're doing this inadvertently).
Typically, it is because you're referencing the same IndexPath for two different layout elements but providing a different frame value for each.
Consider the following:
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
UICollectionViewLayoutAttributes *newAttribute1 = [UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:UICollectionElementKindSectionHeader withIndexPath:indexPath];
newAttribute1.frame = CGRectMake(0, 50, 320, 50);
[attributes addObject:newAttribute1];
UICollectionViewLayoutAttributes *newAttribute2 = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter withIndexPath:indexPath];
newAttribute2.frame = CGRectMake(0, 0, 320, 50);
[attributes addObject:newAttribute2];
Each is using the same IndexPath and thus it causes an NSInternalInconsistencyException
OK, I'm not 100% sure why, but replacing the layoutAttributesForElementsInRect with the following seemed to do the trick:
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
// Call super to get elements
NSMutableArray* answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
NSUInteger maxSectionIndex = 0;
for (NSUInteger idx=0; idx < [answer count]; ++idx) {
UICollectionViewLayoutAttributes *layoutAttributes = answer[idx];
if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell || layoutAttributes.representedElementCategory == UICollectionElementCategorySupplementaryView) {
// Keep track of the largest section index found in the rect (maxSectionIndex)
NSUInteger sectionIndex = (NSUInteger)layoutAttributes.indexPath.section;
if (sectionIndex > maxSectionIndex) {
maxSectionIndex = sectionIndex;
}
}
if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
// Remove layout of header done by our super, as we will do it right later
[answer removeObjectAtIndex:idx];
idx--;
}
}
// Re-add all section headers for sections >= maxSectionIndex
for (NSUInteger idx=0; idx <= maxSectionIndex; ++idx) {
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
if (layoutAttributes) {
[answer addObject:layoutAttributes];
}
}
return answer;
}
I can only imagine that before layoutAttributesForElementsInRect was being called early before the header I had added to the control for the first section was properly initialised, and so programatically determining what headers were present avoided this? Any thoughts would be welcome, but with the above the issue is resolved.
To me this issue was occurring due to Sticky Header Layout, I solved it using PDKTStickySectionHeadersCollectionViewLayout
This happened to me when sectionHeadersPinToVisibleBounds was set to true.
By overriding and passing true in func shouldInvalidateLayout(forBoundsChange: CGRect) rectified it. However, I am not sure of any other side effects that this solution would bring.
The accepted answer (by titaniumdecoy) is correct. I just wanted to share my own experience with this issue as well as the solution I came up with.
I was using a custom decorator to create a divider (separators) between cells and after a while I decided to add headers to sections as well and this caused the internal inconsistency crash.
The solution was to check the indexPath of the current item in the layouts loop and skip the whole loop for that item if it's the first item in it's section.
final class SingleItemWithSeparatorFlowLayout: UICollectionViewFlowLayout {
var skipFirstItem: Bool = false;
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect) ?? [];
let lineWidth = self.minimumLineSpacing;
var decorationAttributes: [UICollectionViewLayoutAttributes] = [];
for layoutAttribute in layoutAttributes where skipFirstItem ? (layoutAttribute.indexPath.item > 0) : true {
// skip the first item in each section
if(layoutAttribute.indexPath.item == 0) {
continue;
}
let separatorAttribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: SeparatorView.ID, with: layoutAttribute.indexPath);
let cellFrame = layoutAttribute.frame;
separatorAttribute.frame = CGRect(x: cellFrame.origin.x, y: cellFrame.origin.y, width: cellFrame.size.width, height: lineWidth);
separatorAttribute.zIndex = Int.max;
decorationAttributes.append(separatorAttribute);
}
return layoutAttributes + decorationAttributes;
}
}
And here is the separator view (it's not directly related to the question but maybe it's useful for future readers)
final class SeparatorView: UICollectionReusableView {
static let ID = "SeparatorView";
override init(frame: CGRect) {
super.init(frame: frame);
self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5);
}
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
self.frame = layoutAttributes.frame;
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented");
}
}
I present a view controller with a uicollectionview. I want the cells to animate in from the top.
My layout is a sublclass of flowlayout, so I override this method:
- (UICollectionViewLayoutAttributes *) initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:indexPath];
CGFloat height = [self collectionViewContentSize].height;
attributes.transform3D = CATransform3DMakeTranslation(0, -height, 0);
return attributes;
}
It almost works, but i see the cells appear (flash) momentarily on the screen before they animate in.
Any ideas why they appear in their final location before the transform is applied, and how I might prevent this?
Rather than setting the transform, change the center:
- (UICollectionViewLayoutAttributes *) initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:indexPath];
CGPoint c = attributes.center;
c.y -= self.collectionViewContentSize.height;
attributes.center = c;
return attributes;
}
Changing the transform can always cause problems with determining visibility, because doing so invalidates the frame property.
I am trying to offset the center on y axis, of all cells below selected cell. I added a property to CollectionViewFlowLayout subclass, called extendedCell, which marks the cell below which I should offset everything else.
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
if(!self.extendedCell)
{
return attributes;
}
else
{
return [self offsetCells:attributes];
}
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attribute = [super layoutAttributesForItemAtIndexPath:indexPath];
if(!self.extendedCell)
{
return attribute;
}
else
{
if(![attribute.indexPath isEqual:self.extendedCell] &&
attribute.center.y >= [super layoutAttributesForItemAtIndexPath:self.extendedCell].center.y)
{
CGPoint newCenter = CGPointMake(attribute.center.x,
attribute.center.y + 180.f);
attribute.center = newCenter;
}
return attribute;
}
}
-(NSArray *)offsetCells:(NSArray *)layoutAttributes
{
for(UICollectionViewLayoutAttributes *attribute in layoutAttributes)
{
if(![attribute.indexPath isEqual:self.extendedCell] &&
attribute.center.y >= [super layoutAttributesForItemAtIndexPath:self.extendedCell].center.y)
{
CGPoint newCenter = CGPointMake(attribute.center.x,
attribute.center.y + 180.0f);
attribute.center = newCenter;
}
}
return layoutAttributes;
}
Turns out that something bad happens on the way, as cells at the bottom disappear. I have a feeling that this has something to do with cells being outside UICollectionView content size, but setting the size while generating layout does not help. Any ideas how to fix that disappearance?
OK turns out I found the bug. Seems that overriding -(CGSize)collectionViewContentSize helps. If any cells lie outside the content size of the collection view they simply disappear. As the content size is set for good before any layout attributes calls, and cells are not allowed to be placed outside of it, collection view just gets rid of them. I thought the content size is based upon cells attributes after they've been set, this is not the case.
-(CGSize)collectionViewContentSize
{
if(self.extendedCell)
{
CGSize size = [super collectionViewContentSize];
return CGSizeMake(size.width, size.height + 180);
}
else
{
return [super collectionViewContentSize];
}
}
Solved it by breaking big cells into minor cells an connected them with a view. Very hacky, but iOS7 will hopefully help.
Check if any returned size of cell have one of border size equals 0.
In my case disappear cause was sizeForItem returned {320,0}
Why UICollectionView with UICollectionViewFlowLayout not show cells, but ask for size?
And for correct size view from nib (with used autolayouts) i use:
+ (CGSize)sizeForViewFromNib:(NSString *)nibName width:(CGFloat)width userData:(id)userData {
UIView *view = viewFromNib(nibName, nil);
[view configForUserData:userData];
CGSize size = [view systemLayoutSizeFittingSize:CGSizeMake(width, SOME_MIN_SIZE) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
return CGSizeMake(width, size.height);
}