UICollectionView是开发中用的比较多的一个控件,本文记录UICollectionView在开发中经常使用的方法总结,包括使用
UICollectionViewFlowLayout
实现Grid布局、添加Header/Footer、自定义layout布局、UICollectionView的其它方面好比添加Cell的点击效果等等html
本文Demo: CollectionViewDemoios
UICollectionView
中有几个重要的概念,理解这几个重要的概念对于使用UICollectionView
有很大的帮助,这个几个概念从用户的数据、布局展现的数据、视图展现的View、UICollectionView
充当的角色这几个维度来展开讲解,这部分讲解的是偏概念的东西,若是你是一个实用主义者,那么能够直接跳到下一部分“UICollectionView和UICollectionViewFlowLayout”查看UICollectionView的简单实用,而后再回过头来回顾下这些概念,这样也是一个比较好的方式git
用户的数据是UICollectionView中的DataSource,DataSource告诉UICollectionView有几个section、每一个section中有几个元素须要展现,这点和UITableView中的DataSource是相似的github
布局展现的数据是UICollectionView中的Layout,Layout告诉UICollectionView每一个section中元素展现的大小和位置,每一个元素展现的位置大小信息是保存在一个UICollectionViewLayoutAttributes
类的对象中,Layout对象会管理一个数组包含了多个UICollectionViewLayoutAttributes
的对象。Layout对应的具体类是UICollectionViewLayout
和UICollectionViewFlowLayout
,UICollectionViewFlowLayout
能够直接使用,最简单的经过设置每一个元素的大小就能够实现Grid布局。若是须要更多了定制设置其余属性好比minimumLineSpacing
、minimumInteritemSpacing
来设置元素之间的间距。数组
DataSource中每一个数据展现须要使用到的是UICollectionViewCell
类对象,通常的经过建立UICollectionViewCell
的子类,添加须要的UI元素进行自定义的布局。可使用registerClass:forCellReuseIdentifier:
方法或者registerNib:forCellReuseIdentifier:
方法注册,而后在UICollectionView的DataSource方法collectionView: cellForItemAtIndexPath:
中使用方法dequeueReusableCellWithIdentifier:
获取到前面注册的Cell,使用item设置急须要展现的数据。bash
另外若是有特殊的Header/Footer需求,须要使用到的是UICollectionReusableView
类,通常也是经过建立子类进行设置自定义的UI。可使用registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
方法或者registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
方法注册,而后在UICollectionView的DataSource方法collectionView: viewForSupplementaryElementOfKind: atIndexPath:
中使用方法dequeueReusableSupplementaryViewOfKind: withReuseIdentifier: forIndexPath:
获取到前面注册的reusableView,而后设置须要展现的数据。app
UICollectionView在这里面充当的角色是一个容器类,是一个中间者,他用于链接DataSource、Layout、UI之间的关系,起到一个协调的做用,CollectionView的角色可使用下面的这张图来标识。
ide
UICollectionView已经为咱们准备好了一个开箱即用的Layout类,就是UICollectionViewFlowLayout
,使用UICollectionViewFlowLayout
能够实现常用到的Grid表格布局,下面了解下UICollectionViewFlowLayout
中经常使用的几个属性的意思以及如何使用和定制UICollectionViewFlowLayout
。布局
UICollectionViewFlowLayout
头文件中定义的属性以下:性能
@property (nonatomic) CGFloat minimumLineSpacing;
@property (nonatomic) CGFloat minimumInteritemSpacing;
@property (nonatomic) CGSize itemSize;
@property (nonatomic) UICollectionViewScrollDirection scrollDirection;
@property (nonatomic) UIEdgeInsets sectionInset;
复制代码
minimumLineSpacing 若是itemSize的大小是同样的,那么真实的LineSpacing就是minimumLineSpacing,若是高度不同,那么这个值回事上一行中Y轴值最大者和当前行中Y轴值最小者之间得高度,行中其它元素的LineSpacing会大于minimumLineSpacing

minimumInteritemSpacing 以下图所示,定义的是元素水平之间的间距,这个间距会大于等于咱们设置的值,由于有可能有可能一行容纳不下只能容纳下N个元素,还有M个单位的空间,这些剩余的空间会被平局分配到元素的间距,那么真实的IteritemSpacing值实际上是(minimumInteritemSpacing + M / (N - 1))
itemSize itemSize表示的是Cell的大小
scrollDirection 以下图所示,表示UICollectionView的滚动方向,能够设置垂直方向UICollectionViewScrollDirectionVertical
和水平方向UICollectionViewScrollDirectionHorizontal

sectionInset 定义的是Cell区域相对于UICollectionView区域的上下左右之间的内边距,以下图所示

在了解了UICollectionViewFlowLayout
的一些概念以后,咱们实现一个以下的表格布局效果
1. UICollectionViewFlowLayout初始化和UICollectionView的初始化
首先使用UICollectionViewFlowLayout对象初始化UICollectionView对象,UICollectionViewFlowLayout对象设置item元素显示的大小,滚动方向,内边距,行间距,元素间距,使得一行恰好显示两个元素,而且元素内边距为5,元素的间距为10,行间距为20,也就是上图的效果。 这边还有一个重要的操做是使用registerClass:forCellWithReuseIdentifier:
方法注册Cell,以备后面的使用。
- (UICollectionView *)collectionView {
if (_collectionView == nil) {
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
CGFloat itemW = (SCREEN_WIDTH - 20) / 2;
CGFloat itemH = itemW * 256 / 180;
layout.itemSize = CGSizeMake(itemW, itemH);
layout.sectionInset = UIEdgeInsetsMake(5, 5, 5, 5);
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
layout.minimumLineSpacing = 20;
layout.minimumInteritemSpacing = 10;
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
_collectionView.backgroundColor = [UIColor whiteColor];
_collectionView.delegate = self;
_collectionView.dataSource = self;
[_collectionView registerClass:[TTQVideoListCell class] forCellWithReuseIdentifier:@"TTQVideoListCell"];
}
return _collectionView;
}
复制代码
2. UICollectionViewDataSource处理
collectionView: numberOfItemsInSection:
返回元素个数collectionView: cellForItemAtIndexPath:
,使用dequeueReusableCellWithReuseIdentifier:
获取重用的Cell,设置Cell的数据,返回CellcollectionView: didSelectItemAtIndexPath:
,处理Cell的点击事件,这一步是非必须的,可是绝大多数场景是须要交互的,点击Cell须要执行一些处理,因此这里也添加上这个方法,在这里作一个取消选择状态的处理// MARK: - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.dataSource.count;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
TTQVideoListCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"TTQVideoListCell" forIndexPath:indexPath];
TTQVideoListItemModel *data = self.dataSource[indexPath.item];
[cell setupData:data];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
TTQVideoListItemModel *data = self.dataSource[indexPath.item];
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
// FIXME: ZYT 处理跳转
}
复制代码
3.数据源
数据源是一个简单的一维数组,以下
- (NSMutableArray *)dataSource {
if (!_dataSource) {
_dataSource = [NSMutableArray array];
// FIXME: ZYT TEST
for (int i = 0; i < 10; i++) {
TTQVideoListItemModel *data = [TTQVideoListItemModel new];
data.images = @"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1534329621698&di=60249b63257061ddc1f922bf55dfa0f4&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fimgad%2Fpic%2Fitem%2Fd009b3de9c82d158e0bd1d998b0a19d8bc3e42de.jpg";
[_dataSource addObject:data];
}
}
return _dataSource;
}
复制代码
4.Cell实现
在这个演示项目中,Cell是经过代码的方式继承UICollectionViewCell
实现的
头文件:
@interface TTQVideoListCell : UICollectionViewCell
- (void)setupData:(TTQVideoListItemModel *)data;
@end
复制代码
实现文件:
@interface TTQVideoListCell()
@property (nonatomic, strong) UIImageView *coverImageView;
@property (nonatomic, strong) UIView *titleLabelBgView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *playCountLabel;
@property (nonatomic, strong) UILabel *praiseCountLabel;
@property (nonatomic, strong) UILabel *statusLabel;
@property (nonatomic, strong) UILabel *tagLabel;
@property (nonatomic, strong) TTQVerticalGradientView *bottomGradientView;
@property (nonatomic, strong) TTQVerticalGradientView *topGradientView;
@property (strong, nonatomic) UIView *highlightView;
@end
@implementation TTQVideoListCell
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupUI];
}
return self;
}
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
if (highlighted) {
self.highlightView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
} else {
self.highlightView.backgroundColor = [UIColor colorWithWhite:0 alpha:0];
}
}
- (void)setupUI {
self.contentView.layer.cornerRadius = 4;
self.contentView.layer.masksToBounds = YES;
[self.contentView addSubview:self.coverImageView];
[self.contentView addSubview:self.topGradientView];
[self.contentView addSubview:self.bottomGradientView];
[self.contentView addSubview:self.titleLabelBgView];
[self.titleLabelBgView addSubview:self.titleLabel];
[self.contentView addSubview:self.playCountLabel];
[self.contentView addSubview:self.praiseCountLabel];
[self.contentView addSubview:self.statusLabel];
[self addSubview:self.tagLabel];
[self addSubview:self.highlightView];
// 布局省略了,具体能够查看git仓库中的代码
}
- (void)setupData:(TTQVideoListItemModel *)data {
self.titleLabel.text = data.title;
self.playCountLabel.text = @"播放次数";
self.praiseCountLabel.text = @"点赞次数";
[self.coverImageView sd_setImageWithURL:[NSURL URLWithString:data.images]];
if (data.status == TTQVideoItemStatusReviewRecommend) {
self.tagLabel.hidden = NO;
self.statusLabel.hidden = YES;
self.tagLabel.text = data.status_desc;
} else {
self.tagLabel.hidden = YES;
self.statusLabel.hidden = NO;
self.statusLabel.text = data.status_desc;
}
}
复制代码
只要以上几个步骤,咱们就能实现一个Grid的表格布局了,若是有其它的Header/Footer的需求,其实也只要增长三个小步骤就能够实现,下面就来实现一个带有Header/Footer效果的CollectionView
UICollectionView中的Header和Footer也是会常用到的,下面经过三个步骤来实现,这三个步骤其实和Cell的步骤是类似的,因此十分简单

**1.注册Header/Footer **
使用registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
方法或者registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
方法注册
[_collectionView registerClass:SimpleCollectionHeaderView.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"SimpleCollectionHeaderView"];
[_collectionView registerClass:SimpleCollectionFooterView.class forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"SimpleCollectionFooterView"];
复制代码
**2.获取Header/Footer **
collectionView: layout: referenceSizeForHeaderInSection:
返回header的高度collectionView: layout: referenceSizeForFooterInSection:
返回footer的高度collectionView: viewForSupplementaryElementOfKind: atIndexPath:
方法,使用方法dequeueReusableSupplementaryViewOfKind: withReuseIdentifier: forIndexPath:
获取到前面注册的reusableView,而后设置须要展现的数据。该方法中的kind参数可使用UICollectionElementKindSectionHeader
、UICollectionElementKindSectionFooter
两个常量来判断是footer仍是header// MARK: 处理Header/Footer
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
return CGSizeMake(SCREEN_WIDTH, 40);
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section {
return CGSizeMake(SCREEN_WIDTH, 24);
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
UICollectionReusableView *supplementaryView = nil;
SectionDataModel *sectionData = self.dataSource[indexPath.section];
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
SimpleCollectionHeaderView* header = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"SimpleCollectionHeaderView" forIndexPath:indexPath];
header.descLabel.text = sectionData.title;
supplementaryView = header;
} else {
SimpleCollectionFooterView* footer = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"SimpleCollectionFooterView" forIndexPath:indexPath];
footer.descLabel.text = [NSString stringWithFormat:@"%@条数据", @(sectionData.items.count)];
supplementaryView = footer;
}
return supplementaryView;
}
复制代码
**3.Header/Footer类实现 **
继承UICollectionReusableView类,而后进行自定义的UI布局便可,下面实现一个简单的Header,只有一个Label显示分类的标题,注意须要使用UICollectionReusableView子类,才能利用CollectionView中的重用机制
头文件
@interface SimpleCollectionHeaderView : UICollectionReusableView
@property (nonatomic, strong) UILabel *descLabel;
@end
复制代码
实现文件
@implementation SimpleCollectionHeaderView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self addSubview:self.descLabel];
self.backgroundColor = [UIColor colorWithWhite:0.95 alpha:0.6];;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.descLabel.frame = CGRectMake(15, 0, self.bounds.size.width - 30, self.bounds.size.height);
}
- (UILabel *)descLabel {
if (!_descLabel) {
_descLabel = [UILabel new];
_descLabel.font = [UIFont systemFontOfSize:18];
_descLabel.textColor = [UIColor colorWithWhite:0.7 alpha:1];
}
return _descLabel;
}
@end
复制代码
自定义Layout为CollectionView的布局提供了最大的灵活性,使用自定义的Layout能够实现复杂的布局视图,下面会经过一个简单的例子来了解下自定义Layout,更加深刻的内容能够查看ClassHierarchicalTree这个开源项目的代码进行学习,Demo项目中自定义布局实现的效果以下:

自定义Layout须要通过如下的几个步骤
做为一个最简单的实践,本文不作预处理,因此步骤只有后面三个,接下来逐个的展开来讲
下面的代码中会使用到下面的几个宏定义的值得意思说明以下:
/** Cell外边距 */
#define VideoListCellMargin 5
/** Cell宽度 */
#define VideoListCellWidth ((SCREEN_WIDTH - VideoListCellMargin * 3) / 2)
/** Cell高度 */
#define VideoListCellHeight (VideoListCellWidth * 265 / 180)
复制代码
下面的代码中会使用到headerHeight
表示的是头部视图的高度,datas
表示的是数据源
@interface TTQVideoListLayout : UICollectionViewLayout
@property (nonatomic, strong) NSArray<TTQVideoListItemModel *> *datas;
/** 头部视图的高度 */
@property (nonatomic, assign) CGFloat headerHeight;
@end
复制代码
ContentSize的概念和ScrollView中contentSize的概念相似,表示的是全部内容占用的大小,下面的代码会根据DataSource数组的大小和headerHeight的值计算最终须要显示的大小
- (CGSize)collectionViewContentSize {
return CGSizeMake(SCREEN_WIDTH, ceil((CGFloat)self.datas.count / (CGFloat)2) * (VideoListCellHeight + VideoListCellMargin) + self.headerHeight + VideoListCellMargin);
}
复制代码
返回值是一个数组,表示的是在UICollectionView可见范围内的item显示的Cell的布局参数,以下图的Visible rect标识的位置中全部元素的布局属性

实现的方式很简单,经过对所有内容的布局属性的遍历,判断是否和显示区域的rect有交集,若是有交集,就把该布局属性对象添加到数组中,最后返回这个数组。
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSMutableArray *array = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < self.datas.count; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
if (!CGRectEqualToRect(attributes.frame, CGRectZero)) {
if (CGRectIntersectsRect(rect, attributes.frame)) {
[array addObject:attributes];
}
}
}
return array;
}
复制代码
这个方法用于返回和单独的IndexPath相关的布局属性对象,根据indexPath中的row参数能够知道元素的位置,而后能够计算出相应所在的位置大小,而后初始化一个UICollectionViewLayoutAttributes对象,设置参数值,返回UICollectionViewLayoutAttributes对象便可
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
if (indexPath.row < self.datas.count) {
id item = self.datas[indexPath.row];
if ([item isKindOfClass:[TTQVideoListItemModel class]]) {
CGFloat originX = (indexPath.row % 2 == 0) ? (VideoListCellMargin) : (VideoListCellMargin * 2 + VideoListCellWidth);
CGFloat originY = indexPath.row/ 2 * (VideoListCellMargin + VideoListCellHeight) + VideoListCellMargin + self.headerHeight;
attributes.frame = CGRectMake(originX, originY, VideoListCellWidth, VideoListCellHeight);
} else {
attributes.frame = CGRectZero;
}
} else {
attributes.frame = CGRectZero;
}
return attributes;
}
复制代码
Cell点击效果是很常用到的,这边主要讲下两种Cell点击效果的实现方式
有两种方法能够实现CollectionViewCell的点击效果,一种是设置CollectionViewCell
的属性selectedBackgroundView
和backgroundView
;另外一种是重写setHighlighted
方法设置自定义的背景View的高亮状态
下图中的左边是点击效果,右边是普通的状态
UIView *selectedBackgroundView = [UIView new];
selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
self.selectedBackgroundView = selectedBackgroundView;
UIView *backgroundView = [UIView new];
backgroundView.backgroundColor = [UIColor clearColor];
self.backgroundView = backgroundView;
复制代码
 这种方式有一个局限性,以下图所示,设置的selectedBackgroundView
和backgroundView
是位于Cell的最底层,若是上面有自定义的图层会覆盖住selectedBackgroundView
和backgroundView
,好比Cell中设置了一个充满Cell视图的ImageView,点击的效果将会不可见。

重写setHighlighted
方法相对来讲是一种灵活性比较高的方法,这种方式和自定义UITableViewCell的高亮状态很相似,setHighlighted
方法中经过判断不一样的状态进行设置任意的UI元素的样式,咱们能够在Cell的最上层添加一个自定义的高亮状态的View,这样高亮的效果就不会由于充满Cell的UI而致使看不见了,代码以下
- (void)setupUI {
// ......
[self addSubview:self.highlightView];
[self.highlightView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self);
}];
}
- (UIView *)highlightView {
if (!_highlightView) {
_highlightView = [UIView new];
_highlightView.backgroundColor = [UIColor clearColor];
_highlightView.layer.cornerRadius = 3;
}
return _highlightView;
}
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
if (highlighted) {
self.highlightView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
} else {
self.highlightView.backgroundColor = [UIColor colorWithWhite:0 alpha:0];
}
}
复制代码
效果以下图:
Collection View Programming Guide for iOS
自定义 Collection View 布局