UICollectionView
最近在逛一些技术论坛的时候,看到过这么一篇文章,说的是iOS9新加入的一些特性。大概的意思就是说iOS9的API中给UICollectionView新添加了一个可以拖拽cell并预测停止点方法,由于笔者写的仓促没有找到那个demo。
当看完那篇文章之后感觉眼前一亮,就像是《红楼梦》中的刘姥姥进大观园一样,满脸写满了好奇和惊喜。但是又因为最近忙着学习巩固前面的东西,今天终于腾出了时间来学习这个控件。
(请原谅我的啰嗦,我想说一下我的学习过程,不想看的朋友可以直接看下面的知识点)
笔者在学习一个新的控件的时候第一步当然是新建一个工程,然后定义一个UICollectionView的对象。但是在创建对象的时候发现一个和一般的控件不太一样的创建方法:
-initWithFrame: collectionViewLayout:
一般来说以苹果的规范比较特殊的方法都是比较重要的方法(这只是个人总结的一个看法,如果不对请指正),但是它要传入一个UICollectionViewLayout的对象,不知道是什么东西就先创建一个匿名的.
UICollectionView *collection = [[UICollectionView alloc]initWithFrame:frame collectionViewLayout:[[UICollectionViewLayout alloc]init]];
创建完对象,进入UICollectionView.h文件看看有什么属性需要什么代理。我们会发现他有两个协议,一个是delegate一个是dataSource。看到这里是不是觉得和UITableView比较像?实际上也是比较像的,所以笔者就很自信的直接遵守协议然后实现必须实现的代理方法。
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 5;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 10;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *ID = @"MyFirstCell";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];
if (cell == nil) {
cell = [[UICollectionViewCell alloc]init];
}
cell.backgroundColor = LYHRandomColor;
return cell;
}
但是在运行的时候发现一个问题,我实现了数据源方法但是运行出来什么都没有。然后笔者就在这三个代理方法中打了断点,发现一个很奇怪的事情,就是UICollectionViewCell的这个代理方法无论如何都无法进入。这就让人很费解了,为什么我明明实现了这个代理方法,但是在运行的时候就不进这个代理方法呢?笔者实在是被弄的不会了,就上网找资料了。王巍@onevcat大神的博客
以下摘自大神的博客:
原来是在创建的时候我传入的是UICollectionViewLayout类型的,苹果提供了一个比较常用的layout是UICollectionViewFlowLayout类型的。
UICollectionViewLayout
UICollectionViewLayout是UICollectionView的精髓,这也是UITableView和UICollectionView最大的不同点。
UICollectionViewLayout可以说是UICollectionView的大脑和中枢,它负责将各个cell、Supplementary View 和 Decoration Views 进行组织,为它们设定各自的属性,包括但不限于:
- 位置
- 尺寸
- 透明度
- 层级关系
- 形状
- 等等等等……
- Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般要合成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性。
Apple为我们提供了一个最简单的可能也是最常用的默认layout对象,UICollectionViewFlowLayout。Flow Layout 简单的说是一个直线对齐的layout,最常见的Grid View形式即为一种Flow Layout配置。
- 首先一个重要的属性是itemSize,它定义了每一个item的大小。通过设定itemSize可以全局地改变所有cell的尺寸,如果想要对某个cell制定尺寸,可以使用-collectionView:layout:sizeForItemAtIndexPath:方法。
- 间隔 可以指定item之间的间隔和每一行之间的间隔,和size类似,有全局属性,也可以对每一个item和每一个section做出设定:
- @property (CGSize) minimumInteritemSpacing
- @property (CGSize) minimumLineSpacing
- -collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
- -collectionView:layout:minimumLineSpacingForSectionAtIndex:
- 滚动方向 由属性scrollDirection确定scroll view的方向,将影响Flow Layout的基本方向和由header及footer确定的section之间的宽度
- UICollectionViewScrollDirectionVertical
- UICollectionViewScrollDirectionHorizontal
- Header和Footer尺寸 同样地分为全局和部分。需要注意根据滚动方向不同,header和footer的高和宽中只有一个会起作用。垂直滚动时section间宽度为该尺寸的高,而水平滚动时为宽度起作用
- @property (CGSize) headerReferenceSize
- @property (CGSize) footerReferenceSize
- -collectionView:layout:referenceSizeForHeaderInSection:
- -collectionView:layout:referenceSizeForFooterInSection:
- 缩进
- @property UIEdgeInsets sectionInset;
- -collectionView:layout:insetForSectionAtIndex:
总结:
一个UICollectionView的实现包括两个必要部分:UICollectionViewDataSource和UICollectionViewLayout,和一个交互部分:UICollectionViewDelegate。而Apple给出的UICollectionViewFlowLayout已经是一个很强力的layout方案了。
所以我们知道了在用UICollectionView的时候Layout是一个精髓,只有在设置了一个合适的Layout之后才能显示数据源的。再结合之前打的断点我们可以推断出它的执行流程是这样的(这仅仅是个人的猜测)
先执行:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
再执行:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
然后再去找相关layout的设置,(不论其他的代理方法)如果没有设置layout那么是不会执行相关cell的代理方法的,只有设置了才执行。
那么我们找到原因了代码修改如下
UICollectionViewFlowLayout *layout= [[UICollectionViewFlowLayout alloc]init];
layout.itemSize = CGSizeMake(80, 80);
layout.sectionInset = UIEdgeInsetsMake(20, 20, 20, 20);
layout.minimumInteritemSpacing = 20;
layout.minimumLineSpacing = 20;
self.collextion = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.height) collectionViewLayout:layout];
self.collextion.delegate = self;
self.collextion.dataSource = self;
[self.view addSubview:self.collextion];
再运行,程序崩溃了!这是为什么呢?我们来看看报错的原因
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'could not dequeue a view of kind: UICollectionElementKindCell with identifier MyFirstCell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
我们会发现这是因为在重用的时候出错了,但是为什么呢,并没有什么问题啊,根据TableView的经验不能错才对啊
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *ID = @"MyFirstCell";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];
if (cell == nil) {
cell = [[UICollectionViewCell alloc]init];
}
cell.backgroundColor = LYHRandomColor;
return cell;
}
重用队列和tableView没有差别啊,难道中间有一些特别的东西?
以下引用了大神的博客
关于重用
为了得到高效的View,对于cell的重用是必须的,避免了不断生成和销毁对象的操作,这与在UITableView中的情况是一致的。但值得注意的时,在UICollectionView中,不仅cell可以重用,Supplementary View和Decoration View也是可以并且应当被重用的。在iOS5中,Apple对UITableView的重用做了简化,以往要写类似这样的代码:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MY_CELL_ID"];
if (!cell) { //如果没有可重用的cell,那么生成一个
cell = [[UITableViewCell alloc] init];
}
//配置cell,blablabla
return cell
而如果我们在TableView向数据源请求数据之前使用-registerNib:forCellReuseIdentifier:
方法为@“MYCELLID”注册过nib的话,就可以省下每次判断并初始化cell的代码,要是在重用队列里没有可用的cell的话,runtime将自动帮我们生成并初始化一个可用的cell。
这个特性很受欢迎,因此在UICollectionView中Apple继承使用了这个特性,并且把其进行了一些扩展。使用以下方法进行注册:
- -registerClass:forCellWithReuseIdentifier:
- -registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
- -registerNib:forCellWithReuseIdentifier:
- -registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
相比UITableView有两个主要变化:一是加入了对某个Class的注册,这样即使不用提供nib而是用代码生成的view也可以被接受为cell了;二是不仅只是cell,Supplementary View也可以用注册的方法绑定初始化了。在对collection view的重用ID注册后,就可以像UITableView那样简单的写cell配置了:
- (UICollectionView*)collectionView:(UICollectionView*)cv cellForItemAtIndexPath:(NSIndexPath*)indexPath {
MyCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@”MY_CELL_ID”];
// Configure the cell's content
cell.imageView.image = ...
return cell;
}
需要吐槽的是,对collection view,取重用队列的方法的名字和UITableView里面不一样了,在Identifier前面多加了Reuse五个字母,语义上要比以前清晰,命名规则也比以前严谨了..不知道Apple会不会为了追求完美而把UITableView中的命名不那么好的方法deprecate掉。
那么我们在创建UICollectionView的时候再加一句话
[self.collextion registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"MyFirstCell"];
所以这时候全部的源代码是这样的:
//
// ViewController.m
// UICollectionView
//
// Created by 厉煜寰 on 15/9/27.
// Copyright © 2015年 SXT. All rights reserved.
//
#import "ViewController.h"
#define LYHRandomColor [UIColor colorWithRed:arc4random_uniform(255)/255.0 green:arc4random_uniform(255)/255.0 blue:arc4random_uniform(255)/255.0 alpha:1]
@interface ViewController ()<UICollectionViewDataSource,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout>
@property (nonatomic, strong) UICollectionView *collextion;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UICollectionViewFlowLayout *layout= [[UICollectionViewFlowLayout alloc]init];
layout.itemSize = CGSizeMake(80, 80);
layout.sectionInset = UIEdgeInsetsMake(20, 20, 20, 20);
layout.minimumInteritemSpacing = 20;
layout.minimumLineSpacing = 20;
self.collextion = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.height) collectionViewLayout:layout];
self.collextion.delegate = self;
self.collextion.dataSource = self;
[self.collextion registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"MyFirstCell"];
[self.view addSubview:self.collextion];
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 5;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 10;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *ID = @"MyFirstCell";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];
if (cell == nil) {
cell = [[UICollectionViewCell alloc]init];
}
cell.backgroundColor = LYHRandomColor;
return cell;
}
@end
再运行的时候我们就能把CollectionView显示在界面上了。当然这只是万里长征第一步,CollectionView比TableView更加复杂更加强大,在接下的实践中再为大家讲解。