UICollectionView and Supplementary View (header)

2019-03-12 11:35发布

Trying to add a a Supplementary view into my UICollectionView as a header. I'm having issues getting it to work.

I use a custom UICollectionViewFlowLayout to return a contentSize that is always at least 1 pixel larger then the frame (I am using a UIFreshControl which will only work if the UICollectionView scrolls, and setting collectionView.contentSize directly does nothing) and to invalidateLayout on sectionInsert and itemSize changes:

-(void)setSectionInset:(UIEdgeInsets)sectionInset {
    if (UIEdgeInsetsEqualToEdgeInsets(super.sectionInset, sectionInset)) {
        return;
    }

    super.sectionInset = sectionInset;

    [self invalidateLayout];

}

-(void) setItemSize:(CGSize)itemSize {
    if (CGSizeEqualToSize(super.itemSize, itemSize)) {
        return;
    }

    super.itemSize = itemSize;

    [self invalidateLayout];
}

- (CGSize)collectionViewContentSize
{
    CGFloat height = [super collectionViewContentSize].height;

    // Always returns a contentSize larger then frame so it can scroll and UIRefreshControl will work
    if (height < self.collectionView.bounds.size.height) {
        height = self.collectionView.bounds.size.height + 1;
    }

    return CGSizeMake([super collectionViewContentSize].width, height);
}

I created a UICollectionReusableView class which is just a UIView with a UILabel:

@implementation CollectionHeaderView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:@"CollectionHeaderView" owner:self options:nil];

        if ([arrayOfViews count] < 1) {
            return nil;
        }

        if (![[arrayOfViews objectAtIndex:0] isKindOfClass:[UICollectionViewCell class]]) {
            return nil;
        }

        self = [arrayOfViews objectAtIndex:0];

        self.headerLabel.text = @"This is a header. There are many like it.";
        self.backgroundColor = [UIColor yellowColor];


    }
    return self;
}

Trying to implement it:

DatasetLayout *collectionViewFlowLayout = [[DatasetLayout alloc] init];
collectionViewFlowLayout.itemSize = CGSizeMake(360, 160);
collectionViewFlowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
collectionViewFlowLayout.sectionInset = UIEdgeInsetsMake(16, 16, 16, 16);
collectionViewFlowLayout.minimumInteritemSpacing = 16;
collectionViewFlowLayout.minimumLineSpacing = 16;
collectionViewFlowLayout.headerReferenceSize = CGSizeMake(0, 100);

UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:collectionViewFlowLayout];
collectionView.translatesAutoresizingMaskIntoConstraints = FALSE;
collectionView.backgroundColor = [UIColor yellowColor];
collectionView.delegate = self;
collectionView.dataSource = self;

I register the class:

[self.collectionView registerClass:[CollectionHeaderView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"CollectionHeaderView"];

and implement the delegate:

-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {

    CollectionHeaderView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"CollectionHeaderView" forIndexPath:indexPath];

    headerView.headerLabel.text = @"Blarg!";

    return headerView;
}

The line

collectionViewFlowLayout.headerReferenceSize = CGSizeMake(0, 100);

causes the error:

*** Assertion failure in -[UICollectionView _createPreparedSupplementaryViewForElementOfKind:atIndexPath:withLayoutAttributes:], /SourceCache/UIKit_Sim/UIKit-2380.17/UICollectionView.m:1150
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView dataSource is not set'

If I comment it out, it runs but no header.

What am I doing wrong or not implementing?

10条回答
做自己的国王
2楼-- · 2019-03-12 12:26

I faced with the similar problem, but my app crashed after I programmatically pop my UICollectionViewController. In some reason (I think it's just a bug in SDK) self.collectionView was alive after its' controller destroy, thus causing this failure:

*** Assertion failure in -[UICollectionView _createPreparedSupplementaryViewForElementOfKind:atIndexPath:withLayoutAttributes:applyAttributes:], /SourceCache/UIKit/UIKit-2935.137/UICollectionView.m:1305
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView dataSource is not set'

The solution is just override -dealloc in UICollectionViewController and release self.collectionView manually. ARC code:

- (void)dealloc {

    self.collectionView = nil;
}

Hope this will save time for somebody.

查看更多
欢心
3楼-- · 2019-03-12 12:26

I also got this annoying error:

*** Assertion failure in -[UICollectionView _createPreparedSupplementaryViewForElementOfKind:atIndexPath:withLayoutAttributes:applyAttributes:], /SourceCache/UIKit/UIKit-3347.44/UICollectionView.m:1400
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView dataSource is not set'

and I found some people solving this error by doing

- (void)dealloc {
    self.collectionView = nil;
}

or in swift:

deinit {
    collectionView = nil
}

however neither of these solved my crash. I tried a few things out and the following "hack" solved this annoying bug:

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
    if collectionView.dataSource != nil {
        return someHeaderSize
    } else {
        return CGSizeZero
    }
}
查看更多
祖国的老花朵
4楼-- · 2019-03-12 12:34

A iOS9 bug, In viewcontroller's dealloc set layer delegate nil.

- (void)dealloc{

    if ([[[UIDevice currentDevice] systemVersion] hasPrefix:@"9"]) {
        self.collectionView.layer.delegate = nil;
    }
}
查看更多
手持菜刀,她持情操
5楼-- · 2019-03-12 12:39

This answer is similar to the others where you create a method that checks for the data source, but it's a bit simpler. It seems the issue is related to the collection view's layout sticking around after the collection has been released. So I zeroed out the layout's properties as below and the error disappeared.

deinit {
    let layout = self.collectionView.collectionViewLayout as! UICollectionViewFlowLayout
    layout.estimatedItemSize = CGSizeZero
    layout.sectionInset = UIEdgeInsetsZero
    layout.headerReferenceSize = CGSizeZero
}
查看更多
登录 后发表回答