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設置急須要展現的數據。app
另外若是有特殊的Header/Footer需求,須要使用到的是UICollectionReusableView
類,通常也是經過建立子類進行設置自定義的UI。可使用registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
方法或者registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
方法註冊,而後在UICollectionView的DataSource方法collectionView: viewForSupplementaryElementOfKind: atIndexPath:
中使用方法dequeueReusableSupplementaryViewOfKind: withReuseIdentifier: forIndexPath:
獲取到前面註冊的reusableView,而後設置須要展現的數據。ide
UICollectionView在這裏面充當的角色是一個容器類,是一箇中間者,他用於鏈接DataSource、Layout、UI之間的關係,起到一個協調的做用,CollectionView的角色可使用下面的這張圖來標識。
佈局
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 佈局