I'm using collection view with custom layout, Calling webservice I received first time from the API returns 20 objects, and the second time it will return 1 object, while reloading the data applications throws the following error.
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: UICollectionView received layout attributes for a cell with an index
path that does not exist: {length = 2, path
= 0 - 0}
Code piece of creating new layout
-(void)doNewLayout
{
id<UICollectionViewDelegateJSPintLayout> delegate = (id<UICollectionViewDelegateJSPintLayout>)self.collectionView.delegate;
// get column width from delegate. If the method isn't implemented fall back to our property
NSUInteger columnWidth = self.columnWidth;
if(delegate && [delegate respondsToSelector:#selector(columnWidthForCollectionView:layout:)])
{
columnWidth = [delegate columnWidthForCollectionView:self.collectionView
layout:self];
}
// find out how many cells there are
NSUInteger cellCount = [self.collectionView numberOfItemsInSection:0];
// get max number of columns from the delegate. If the method isn't implemented, fall back to our property
NSUInteger maximumNumberOfColumns = self.numberOfColumns;
if(delegate && [delegate respondsToSelector:#selector(maximumNumberOfColumnsForCollectionView:layout:)]){
maximumNumberOfColumns = [delegate maximumNumberOfColumnsForCollectionView:self.collectionView layout:self];
}
// build an array of all the cell heights.
NSMutableArray* cellHeights = [NSMutableArray arrayWithCapacity:cellCount];
for(NSUInteger cellIndex = 0; cellIndex < cellCount; ++cellIndex)
{
CGFloat itemHeight = self.itemHeight; // set default item size, then optionally override it
if(delegate && [delegate respondsToSelector:#selector(collectionView:layout:heightForItemAtIndexPath:)])
{
itemHeight = [delegate collectionView:self.collectionView
layout:self
heightForItemAtIndexPath:[NSIndexPath indexPathForItem:cellIndex
inSection:0]];
}
cellHeights[cellIndex] = #(itemHeight);
}
// now build the array of layout attributes
self.pendingLayoutAttributes = [NSMutableArray arrayWithCapacity:cellCount];
// will need an array of column heights
CGFloat* columnHeights = calloc(maximumNumberOfColumns,sizeof(CGFloat)); // calloc() initializes to zero.
CGFloat contentHeight = 0.0;
CGFloat contentWidth = 0.0;
for(NSUInteger cellIndex = 0; cellIndex < cellCount; ++cellIndex)
{
CGFloat itemHeight = [cellHeights[cellIndex] floatValue];
// find shortest column
NSUInteger useColumn = 0;
CGFloat shortestHeight = DBL_MAX;
for(NSUInteger col = 0; col < maximumNumberOfColumns; ++col)
{
if(columnHeights[col] < shortestHeight)
{
useColumn = col;
shortestHeight = columnHeights[col];
}
}
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:cellIndex
inSection:0];
UICollectionViewLayoutAttributes* layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
layoutAttributes.size = CGSizeMake(columnWidth,itemHeight);
layoutAttributes.center = CGPointMake((useColumn * (columnWidth + self.interitemSpacing)) + (columnWidth / 2.0),columnHeights[useColumn] + (itemHeight / 2.0));
self.pendingLayoutAttributes[cellIndex] = layoutAttributes;
columnHeights[useColumn] += itemHeight;
if(columnHeights[useColumn] > contentHeight)
contentHeight = columnHeights[useColumn];
CGFloat rightEdge = (useColumn * (columnWidth + self.interitemSpacing)) + columnWidth;
if(rightEdge > contentWidth)
contentWidth = rightEdge;
columnHeights[useColumn] += self.lineSpacing;
}
self.contentSize = CGSizeMake(contentWidth,contentHeight+100);
free(columnHeights);
}
Any Quick solution would be really appreciated.Thanks
There are couple of things need consideration , as you are custom layout then you need to create layoutAttribute for each indexPath. In your case your data source array count and offerModel.arrayOffers and self.pendingLayoutAttributes should be same, if not then it might be problem and its crushable problem if offerModel.arrayOffershave more items then self.pendingLayoutAttributes.
If you are loading data async then make sure when you are adding rows in arraysOffers also add layoutAttributes in customLayout pendLayoutAttributes,which I think you are not doing at the moment, do that by adding a method and provide new indexPaths to that which create layoutAttributes.
I usually do like this
- (void)insertItems:(NSArray*)items
{
NSInteger startIndex = <start index for new item>;
[self.items addObjectsFromArray:items];
[self calculateLayoutForItems:items startIndex:startIndex];
}
This method will calculate layoutAttributes
- (void)calculateLayoutForItems:(NSArray*)items startIndex:(NSInteger)startIndex
{
// collection view and loop over them as well and nest indexPath creation
NSInteger section = 0;
NSInteger endIndex = self.items.count;
// Lets create indexPaths for provided items and get there frames
for (NSInteger index = startIndex ;index < endIndex; index++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:section];
CGRect frame = [self frameForItemAtIndexPath:indexPath];// call your method for frame at indexPath
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
itemAttributes.frame = frame;
self.pendingLayoutAttributes[indexPath] = itemAttributes; // you can use your index
}
}
Now when you got more items in data source call this method on customLayoutObject [self.customLayout insertItems:newItemsArray];
Also if you have stored properties then its worth overriding invalidate method in custom layout to reset all properties to initial state,Then
you can just invalidate customLayout before reloadData method, then collection view will force it to compute layout again.
Related
I'm currently following this tutorial (https://www.raywenderlich.com/99087/swift-expanding-cells-ios-collection-views) which creates a custom UICollectionViewLayout, the problem is that the tutorial is written in Swift but I'm trying to convert it to Objective-C for my project.
The first part of reproducing the tutorial into Objective-C was fine and i've gotten to this stage.
Though, in the 2nd part when we're suppose to create a custom UICollectionViewLayout, and upon changing in the storyboard the Layout of CollectionView to Custom and setting the custom class, a blank screen appears.
Below are the codes that I'd reproduced from the tutorial from Swift to Objective-C:
#implementation TimetableCustomLayout{
NSMutableArray *cache;
}
-(NSInteger)featuredItemIndex{
CGFloat dragOffset = 180;
return MAX(0, self.collectionView.contentOffset.y - dragOffset);
}
-(CGFloat)nextItemPercentageOffset{
CGFloat dragOffset = 180;
return (self.collectionView.contentOffset.y / dragOffset) - [self featuredItemIndex];
}
-(CGFloat)width{
return CGRectGetWidth(self.collectionView.bounds);
}
-(CGFloat)height{
return CGRectGetHeight(self.collectionView.bounds);
}
-(NSInteger)numberOfItems{
//Will be replaced with dynamic value later
return 5;
}
-(CGSize)collectionViewContentSize{
CGFloat dragOffset = 180;
return CGSizeMake([self height], [self width]);
}
-(void)prepareLayout{
[super prepareLayout];
cache = [[NSMutableArray alloc]initWithObjects:[UICollectionViewLayoutAttributes class], nil];
self.standardHeight = 100;
self.featuredHeight = 280;
CGRect frame = CGRectZero;
CGFloat y = 0;
for(int item = 0; item < 5; item ++){
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:0];
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.zIndex = item;
CGFloat height = self.standardHeight;
if(indexPath.item == [self featuredItemIndex]){
CGFloat yOffset = self.standardHeight * [self nextItemPercentageOffset];
y = self.collectionView.contentOffset.y - yOffset;
height = self.featuredHeight;
}else if(indexPath.item == ([self featuredItemIndex] + 1) && indexPath.item != [self numberOfItems]){
CGFloat maxY = y + self.standardHeight;
height = self.standardHeight + MAX((self.featuredHeight - self.standardHeight) * [self nextItemPercentageOffset], 0);
y = maxY - height;
}
frame = CGRectMake(0, y, [self width], [self height]);
attributes.frame = frame;
[cache addObject:attributes];
y = CGRectGetMaxY(frame);
}
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
[super layoutAttributesForElementsInRect:rect];
NSMutableArray *layoutAttributes = [[NSMutableArray alloc]initWithObjects:[UICollectionViewLayoutAttributes class], nil];
NSLog(#"%#", cache);
for(UICollectionViewLayoutAttributes *attributes in cache){
if(CGRectIntersectsRect(attributes.frame, rect)){
[layoutAttributes addObject:attributes];
}
}
return layoutAttributes;
}
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return true;
}
#end
I'm also getting error, Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[UICollectionViewLayoutAttributes frame]: unrecognized selector.
I believe that the error is possibly due to incorrect translation from Swift to Objective-C, particularly this line,
Swift:
var cache = [UICollectionViewLayoutAttributes]()
Objective-C:
NSMutableArray *layoutAttributes = [[NSMutableArray alloc]initWithObjects:[UICollectionViewLayoutAttributes class], nil];
This is my first question on StackOverflow, any feedback and help would be greatly appreciated, thanks in advance.
You are adding a reference to the UICollectionViewLayoutAttributes class to the cache and layoutAttributes array on initialization. And then, calling frame property to the class reference (notice the + on the error message that denotes a class method, where instance methods use -).
Replace this line:
NSMutableArray *layoutAttributes = [[NSMutableArray alloc]initWithObjects:[UICollectionViewLayoutAttributes class], nil];
With this:
NSMutableArray *layoutAttributes = [[NSMutableArray alloc] init];
The same applies to the cache variable initialization.
You can also use type-safe arrays in Objective-C using generics:
NSMutableArray<UICollectionViewLayoutAttributes*> *layoutAttributes = [[NSMutableArray alloc] init];
More information here.
I have a UICollectionView in a 3x3 grid layout displaying images.
The UICollectionView has "pagingEnabled" set to YES, inorder to "page" between pages with the 3x3 grid with 9 images.
This has been accomplished using a custom UICollectionViewLayout:
#implementation HorizontalCollectionViewLayout
- (instancetype)init
{
self = [super init];
if (self) {
}
return self;
}
- (CGSize)collectionViewContentSize
{
// We should return the content size. Lets do some math:
NSInteger verticalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.height / self.itemSize.height);
NSInteger horizontalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.width / self.itemSize.width);
NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount;
NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:0];
NSInteger numberOfPages = (NSInteger)ceilf((CGFloat)numberOfItems / (CGFloat)itemsPerPage);
CGSize size = self.collectionView.bounds.size;
size.width = numberOfPages * self.collectionView.bounds.size.width;
return size;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// Get all the attributes for the elements in the specified frame
NSArray *allAttributesInRect = [super layoutAttributesForElementsInRect:rect];
NSArray *attributesArray = [[NSArray alloc] initWithArray:allAttributesInRect copyItems:YES];
NSInteger verticalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.height / self.itemSize.height);
NSInteger horizontalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.width / self.itemSize.width);
NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount;
for (NSInteger i = 0; i < attributesArray.count; i++) {
UICollectionViewLayoutAttributes *attributes = attributesArray[i];
NSInteger currentPage = (NSInteger)floor((double)i / (double)itemsPerPage);
NSInteger currentRow = (NSInteger)floor((double)(i - currentPage * itemsPerPage) / (double)horizontalItemsCount);
NSInteger currentColumn = i % horizontalItemsCount;
CGRect frame = attributes.frame;
frame.origin.x = self.itemSize.width * currentColumn + currentPage * self.collectionView.bounds.size.width;
frame.origin.y = self.itemSize.height * currentRow;
CGRect f = UIEdgeInsetsInsetRect(frame, UIEdgeInsetsMake(2, 2, 5, 2));
attributes.frame = f;
}
return attributesArray;
}
#end
This is working as expected, as it creates a new page when the first page contains 9 images.
What I want to accomplish then, is to create a static / fixed "next page" button in the right bottom corner of grid at index 8, 17, 26, 35 ... based on the number of pages. (A button at index 8 if the collectionview only has 1 page, a button at index 8 and index 17 if the collectionview only has 2 pages ... and so on). Number of images and pages can vary based on how many images the user uploads to the app.
The following picture illustrates the idea:
Any tips on how I can accomplish this?
What logic should go into "collectionView:itemForIndexPath:"?
In your cellForItemAtIndexPath look at the indexPath.item for the number you are interested in replacing and return a collection view cell for your [ > ] button.
The way the collection view works is that you are never really concerned with the visible cells on the screen (your UICollectionViewLayout is handling that). You are only returning a cell for the indexPath you are given.
Create a dedicated UICollectionViewCell subclass just for this [ > ] cell with its own reuse identifier. It will simplify your code and you don't need to touch your current image cells.
I am new to Collectionview, but not to iOS. I am using custom Flowlayout for my collectionview. I need to return contentsize based on current number of items returned by CollectionView's data source. Is there anyway to know how many items are there currently in flowlayout's collectionview?
#interface LatestNewsFlowLayout : UICollectionViewFlowLayout
-(id)initWithSize:(CGSize) size;
#end
#implementation LatestNewsFlowLayout
-(id)initWithSize:(CGSize) size {
self = [super init];
if (self) {
self.itemSize = size;
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sectionInset = UIEdgeInsetsMake(0, 10.0, 0, 0);
self.minimumLineSpacing = 5;
}
return self;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)oldBounds {
return YES;
}
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
for(int i = 1; i < [answer count]; ++i) {
UICollectionViewLayoutAttributes *currentLayoutAttributes = answer[i];
UICollectionViewLayoutAttributes *prevLayoutAttributes = answer[i - 1];
NSInteger maximumSpacing = 5;
NSInteger origin = CGRectGetMaxX(prevLayoutAttributes.frame);
if(origin + maximumSpacing + currentLayoutAttributes.frame.size.width < self.collectionViewContentSize.width) {
CGRect frame = currentLayoutAttributes.frame;
frame.origin.x = origin + maximumSpacing;
currentLayoutAttributes.frame = frame;
}
}
return answer;
}
-(CGSize) collectionViewContentSize
{
CGSize size;
int numOfItems = ???
size.width = 100 * numOfItems;
size.height = 100;
return size;
}
The UICollectionViewFlowLayout is a subclass of UICollectionViewLayout. It should therefore have access to the UICollectionView which should know how many items there are.
[self.collectionView numberOfItemsInSection:0];
You may need to iterate over the section to get the total number of items if you have more that one section.
You can get the number of sections similarly:
[self.collectionView numberOfSections];
Hope this helps! :)
Swifty solution is next:
let itemsCount = Array(0..<collectionView.numberOfSections)
.map { collectionView.numberOfItems(inSection: $0) }
.reduce(0, +)
Now itemsCount contains total number of collection view items across all sections.
If you make a property/outlet called collectionView, try this:
for (int i = 0; i<[collectionView numberOfSections];i++) {//Iterate through all the sections in collectionView
for (int j = 0; j<[collectionView numberOfItemsInSection:i]; j++) {//Iterate through all the rows in each section
numOfItems++;
}
}
In Swift, you can write the following (provided that you have a data source with sections and items):
let totalItemCount = sections.reduce(0) { result, section -> Int in
return result + section.items.count
}
Just replace section with your data source, and items with the item collection name.
+ (NSInteger)countInCollectionView:(UICollectionView *)collectionView {
NSInteger itemCount = 0;
for( int section = 0; section < [collectionView numberOfSections]; ++section ){
itemCount += [collectionView numberOfItemsInSection:section];
}
return itemCount;
}
I like Vadim's answer because it is swifty and clean, but we're creating an array from a range and looping it twice (map and reduce). I'd suggest a similar solution using directly a range with only one loop (reduce):
let totalItems = (0..<collectionView.numberOfSections)
.reduce(0) { res, cur in
res + collectionView.numberOfItems(inSection: cur)
}
In my case I ended up creating an extension for UICollectionView:
extension UICollectionView {
var totalItems: Int {
(0..<numberOfSections).reduce(0) { res, cur in
res + numberOfItems(inSection: cur)
}
}
}
And then just:
collectionView.totalItems
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'm wondering if there is a way to make a UICollectionView row or header stay still similar to the freeze panes function in a spreadsheet program.
I basically want the first column and row to stay still while the rest is pannable.
Is this possible to do with the UICollectionView?
I have found that the following code will set the first row/section as always visable and will scroll with the appropriate section/row:
- (NSArray *) layoutAttributesForElementsInRect: (CGRect) rect
{
NSMutableArray *attributes = [NSMutableArray array];
for (NSInteger section = 0; section < self.collectionView.numberOfSections; section++)
for (NSInteger item = 0 ; item < [self.collectionView numberOfItemsInSection: section]; item++)
{
UICollectionViewLayoutAttributes *layout = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:item inSection:section]];
if(section==0 || item==0)
{
UICollectionView * const cv = self.collectionView;
CGPoint const contentOffset = cv.contentOffset;
CGPoint origin = layout.frame.origin;
if(item==0)
{
origin.x = contentOffset.x;
layout.zIndex = 1022;
}
if(section==0)
{
origin.y = contentOffset.y;
layout.zIndex = 1023;
if(item==0)layout.zIndex = 1024;
}
layout.frame = (CGRect)
{
.origin = origin,
.size = layout.frame.size
};
}
[attributes addObject:layout];
}
return attributes;
}