iOS-UICollectionView 自定义瀑布流无法添加SectionHeaderView的问题解决
前言
最近在项目中需要自定义一个UICollectionView瀑布流并且为Section添加一个HeaderView。但是以平时的方法设置之后并没有实现,甚至代理方法都没有执行,在查阅资料后得到了解决方法,在此记录一下。
自定义瀑布流
瀑布流创建
首先,我们得创建一个自定义的瀑布流,或者从网上搜一个,但是搜索到的好几个都出现了Cell数量不符的问题,这里我找到了一个数量正确且轻量好用的瀑布流:XRWaterfallLayout ,在下载下来使用之后,实现了我想要的瀑布流效果,但是在添加SectionHeaderView的时候却无法添加,所以我对其进行了改动。
问题原因分析
UIKit提供的瀑布流 UICollectionViewFlowLayout包含了一个设置表头的属性 headerReferenceSize 而自定义的瀑布流是继承自UICollectionViewLayout就不包含这个属性。且自定义瀑布流的布局中没有为表头提供位置。这两个原因导致了对表头的设置无效,就算执行了代理方法也不会显示到屏幕上。知道原因之后就能对症下药了!
代码修改
1.添加一个headerReferenceSize属性
#pragma mark - 属性
//添加一个表头的size
@property (nonatomic, assign) CGSize headerReferenceSize;
//总共多少列,默认是2
@property (nonatomic, assign) NSInteger columnCount;
2.在初始化中设置数值
在初始化中设置数值,这里设置成了 CGSizeZero 可以根据自己的情况而定。对于不同的 Section 需要设置不同高度的情况,可以参考下面的原生方法,自定义一个代理来实现。
@protocol UICollectionViewDelegateFlowLayout <UICollectionViewDelegate>
@optional
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
我的需求只有一个Section,所以我就直接设置了
#pragma mark- 构造方法
- (instancetype)init {
if (self = [super init]) {
self.columnCount = 2;
self.headerReferenceSize = CGSizeZero;
}
return self;
}
- (instancetype)initWithColumnCount:(NSInteger)columnCount {
if (self = [super init]) {
self.columnCount = columnCount;
self.headerReferenceSize = CGSizeZero;
}
return self;
}
3.添加头部视图布局到Section布局中
//布局前的准备工作
- (void)prepareLayout {
[super prepareLayout];
//初始化字典,有几列就有几个键值对,key为列,value为列的最大y值,初始值为上内边距
for (int i = 0; i < self.columnCount; i++) {
self.maxYDic[@(i)] = @(self.sectionInset.top);
}
//根据collectionView获取总共有多少个item
NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
[self.attributesArray removeAllObjects];
//添加头部视图布局
UICollectionViewLayoutAttributes * layoutHeader = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathWithIndex:0]];
layoutHeader.frame = CGRectMake(0, 0, self.headerReferenceSize.width, self.headerReferenceSize.height);
[self.attributesArray addObject:layoutHeader];
//为每一个item创建一个attributes并存入数组
for (int i = 0; i < itemCount; i++) {
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
[self.attributesArray addObject:attributes];
}
}
4.计算collectionView的contentSize
计算collectionView的contentSize中也要加上headerReferenceSize.height
//计算collectionView的contentSize
- (CGSize)collectionViewContentSize {
__block NSNumber *maxIndex = @0;
//遍历字典,找出最长的那一列
[self.maxYDic enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSNumber *obj, BOOL *stop) {
if ([self.maxYDic[maxIndex] floatValue] < obj.floatValue) {
maxIndex = key;
}
}];
//collectionView的contentSize.height就等于最长列的最大y值+下内边距+头部视图高
return CGSizeMake(0, [self.maxYDic[maxIndex] floatValue] + self.sectionInset.bottom + self.headerReferenceSize.height);
}
5.让每个Item向下偏移
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
//根据indexPath获取item的attributes
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
//获取collectionView的宽度
CGFloat collectionViewWidth = self.collectionView.frame.size.width;
//item的宽度 = (collectionView的宽度 - 内边距与列间距) / 列数
CGFloat itemWidth = (collectionViewWidth - self.sectionInset.left - self.sectionInset.right - (self.columnCount - 1) * self.columnSpacing) / self.columnCount;
CGFloat itemHeight = 0;
//获取item的高度,由外界计算得到
if (self.itemHeightBlock) itemHeight = self.itemHeightBlock(itemWidth, indexPath);
else {
if ([self.delegate respondsToSelector:@selector(waterfallLayout:itemHeightForWidth:atIndexPath:)])
itemHeight = [self.delegate waterfallLayout:self itemHeightForWidth:itemWidth atIndexPath:indexPath];
}
//找出最短的那一列
__block NSNumber *minIndex = @0;
[self.maxYDic enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSNumber *obj, BOOL *stop) {
if ([self.maxYDic[minIndex] floatValue] > obj.floatValue) {
minIndex = key;
}
}];
//根据最短列的列数计算item的x值
CGFloat itemX = self.sectionInset.left + (self.columnSpacing + itemWidth) * minIndex.integerValue;
//item的y值 = 最短列的最大y值 + 行间距
CGFloat itemY = [self.maxYDic[minIndex] floatValue] + self.rowSpacing;
//更新字典中的最大y值
self.maxYDic[minIndex] = @(itemY + itemHeight);
//设置attributes的frame
attributes.frame = CGRectMake(itemX, itemY + self.headerReferenceSize.height, itemWidth, itemHeight);
return attributes;
}
注意:在原先的代码里,更新字典中的最大Y值是这样写的
//设置attributes的frame
attributes.frame = CGRectMake(itemX, itemY, itemWidth, itemHeight);
//更新字典中的最大y值
self.maxYDic[minIndex] = @(CGRectGetMaxY(attributes.frame));
如果直接将headerReferenceSize.height 加到attributes的frame中,每一个item的Y都会加上这个高度,间隙就会变大。正确的添加方法是:将更新最大值放到设置attributes的frame前面。
//更新字典中的最大y值
self.maxYDic[minIndex] = @(itemY + itemHeight);
//设置attributes的frame
attributes.frame = CGRectMake(itemX, itemY + self.headerReferenceSize.height, itemWidth, itemHeight);
collectionView的创建
对于collectionView的创建,就需要将布局换成自定义的瀑布流即可
#pragma mark - Lazy
- (UICollectionView *)collectionView {
if (!_collectionView) {
XRWaterfallLayout *waterfall = [XRWaterfallLayout waterFallLayoutWithColumnCount:2];
waterfall.delegate = self;
// 在这里设置头部试图的尺寸
waterfall.headerReferenceSize = CGSizeMake(SCREEN_WIDTH, 30);
[waterfall setColumnSpacing:10 rowSpacing:10 sectionInset:UIEdgeInsetsMake(10, 10, 10, 10)];
_collectionView = [[UICollectionView alloc] initWithFrame:UniformFrame collectionViewLayout:waterfall];
_collectionView.delegate = self;
_collectionView.dataSource = self;
_collectionView.backgroundColor = UIColor.whiteColor;
_collectionView.alwaysBounceVertical = YES;
[_collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:NewHomeVCCellIdentifier];
// 必须有!!!
[_collectionView registerClass:[NewHomeSectionHeaderView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:NewHomeVCSectionHeaderIdentifier];
}
return _collectionView;
}
#pragma mark - UICollectionViewDelegate & UICollectionViewDataSource
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
if(kind == UICollectionElementKindSectionHeader){
NewHomeSectionHeaderView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:NewHomeVCSectionHeaderIdentifier forIndexPath:indexPath];
return headerView;
} else {
// 注意,在设置了footerView的情况下是不能返回nil的,不然会崩溃!
return nil;
}
}
总结
这个问题考察的是程序员对于瀑布流布局的熟悉程度,其中涉及到了瀑布流的具体布局、UICollectionView的delegate&dataSource方法实现、UICollectionElementKindSectionHeader的设置方法等知识点。也要告诫自己,出现问题要冷静分析、断点查看流程、剖析问题原因、和同事沟通、查找资料、从根本上解决问题,不能急躁。
参考文献
《iOS之简单瀑布流的实现》
《iOS - UICollectionView 瀑布流 添加表头视图的坑》
《iOS tableView、collectionView添加SectionHeader》
《iOS开发之UICollectionViewController系列(二) :详解CollectionView各种回调》