每个CollectionView都需要一个DataSource对象. DataSource对象是内容提供者. 它可以是应用程序的数据模型中的对象,也可以是管理集合视图的视图控制器。数据源的唯一要求是它必须能够提供集合视图需要的内容信息,例如有多少item,以及显示这些项时要使用哪些view。
Delegate对象用来管理内容展现和交互. 虽然delegate的主要工作是管理突出显示和选择cell,但是可以扩展它以提供附加信息. 例如,flow layout扩展了delegate基本行为来定制布局度量,例如单元格的大小和它们之间的间隔.
DataSource管理内容信息
datasource对象用来管理CollectionView中显示的内容, 他必须遵循UICollectionViewDataSource
协议, 该协议定义了你需要实现的一些基本行为和方法. DataSource的工作就是需要回答CollectionView如下的问题:
- CollectionView中包含多少个section
- 每一个section中包含的item有多少.
- 用什么view来展示item中的内容.
section和item是CollectionView内容的基本组织原则。CollectionView通常具有至少一个section,每个section中包含0个或者多个item, item用来展示内容,而section用来管理一组itme。例如,照片应用程序可以使用section来表示照片的单一相册或当天拍摄的一组照片。
collectionView通过NSIndexPath
来引用它包含的内容. CollectionView使用layout对象提供的index path信息去定位一个item, index path中包含两个number,一个代表item,另一个代表section. 对于supplementary view和decoration view来说, index path包一个任意值来与之对应. 与supplementary和decoration绑定的indexpath的意义取决于你的APP,尽管第一个index代表DataSource中的section. view的indexpath主要作用用来定位而不是它具体的意义.
注意:尽管标的的index path支持多层级, CollectionView中的cell只支持
section
和item
两个层级, 这个和UITableView
类似. Supplementary和decoration的index path更加复杂. 如果某个item的索引路径层级>1, 那索引路径的第一个值一般指该item属于哪个section(组)。传统上,只有第二个索引是必要的,但补充和装饰视图并不局限于只有两个。在设计数据源时要记住这一点。
不管你在DataSource对象中如何组织section和item,最终的显示还是要靠layout对象. 不同的布局对于显示section和item是不一样的, 如下图所示,使用flowLayout的会将section在竖直方向连续排列,而自定义的布局对象的话,section的排列是非线性的,这个取决于你使用的layout对象.
设计你的数据对象
一个有效的data source对象使用section和item来帮助管理底层数据对象. 将你的数据组织成section和item将有助于你实现data source中的方法. 因为DataSource方法将会频繁调用,所以你实现的DataSource方法应该能快地速获取数据.
一个简单但不唯一的方法是将你的数据保存在多维数组中,如下图所示. 多维数组的第一层对应section,一个section包含一个array,一个item对应sectionArray中的某一个元素.
当你设计数据结构时, 你可以从数组入手,再进行更进一步的设计. 通常,你设计的数据结构不应该成为性能的瓶颈. CollectionView从DataSource中获取显示多少内容, 以及和内容对应的view. 如果layout对象只依靠DataSource中的数据,那么当数据很多时就会产生很多性能问题.
关于CollectionView中的内容
CollectionView要知道有多少section和每个section中有多少item,这些都是由DataSource中的方法回答. 当以下一些动作发送时才会触发DataSource回答这些问题:
- CollectionView第一次显示时
- 更新CollectionView的DataSource对象
- 手动调用
reloadData
方法 - CollectionView调用
performBatchUpdates:comletion:
来进行move(移动),insert(插入),delete(删除)操作
在numberOfSectionsInCollectionView:
中返回section的个数, 在collectionView:numberOfItemsInSection:
中返回item的个数. collectionView:numberOfItemsInSection:
必须实现, numberOfSectionsInCollectionview:
是可选的, 默认section个数为1. 两个方法的返回类型都是整型.
如果你实现图2-1中的DataSource,那么实现的方法就如代码清单2-1所示.
代码清单2-1 返回section和item的个数
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView*)collectionView {
// _data is a class member variable that contains one array per section.
return [_data count];
}
- (NSInteger)collectionView:(UICollectionView*)collectionView numberOfItemsInSection:(NSInteger)section {
NSArray* sectionArray = [_data objectAtIndex:section];
return [sectionArray count];
}
配置Cells和Supplementary View
data source对象还有一个比较重要的作用是提供collectionView中用来显示内容的view(cell). collectionView不会跟踪APP内容的变化, 它只是简单地将要显示的view和layout信息结合起来. 所以,如何展示内容是APP的责任.
当DataSource对象报告了section和item后,collectionView请求layout对象提供内容的layout attribute. 在某一时刻, collectionView要求layout对象提供一个特定矩形中(通常指看见矩形)元素, 保存在一个列表中. collectionView开始使用刚才的列表来请求DataSource相对应的cell和supplementary视图. 为了提供这些cells和supplementary视图, 你的代码必须实现:
- 在storyboard中插入cell和view(可选地,为每种支持的单元格或视图注册一个类或NIB文件。)
- 在DataSource中,dequeue和配置合适的cell或view.
为了确保以最有效的方式使用cell和supplementary视图,collectionView负责为您创建这些对象。每个collectionView内部维持一个未使用的cell和一个未使用的supplementary视图队列. 从collectionView中来dequeue你想要的view, 而不是创建你想要的view. 如果重用队列中有你想要的视图,则集合视图准备并将其快速返回给您。如果没有,则集合视图使用已注册的类或NIB文件创建新视图并将其返回给您。因此,每次当你删除一个单元格或视图时,你总是得到一个现成的对象。
重用标识符(Reuse identifiers)使得可以注册多种类型的cell和多种类型的supplementary视图。重用标识符是用来区分已注册的单元格和视图类型的字符串。字符串的内容仅与数据源对象相关。但是,当请求视图或单元格时,可以使用提供的index path来确定您可能想要哪种类型的视图或cell,然后将适当的重用标识符传递给dequeue方法。
注册cells和supplementary视图
你可以在storyboard或者使用代码来配置collectionView中的cell和其他view
- 在storyboard中配置cell和view. 当在storyboard中配置cell和supplementary, 你可以拖拽item和其他view到collectionView中. 这样就可以创建collectionView和cell对应的关系.
- 对于cell,从对象库中拖动集合视图cell,并将其放到collectionView中. 将自定义类和cell的可重用视图标识符设置为适当的值.
- 对于supplementary视图,从对象库中拖动重用的view到collectionView中. 将自定义类和view的可重用视图设置标识符设置适当的值.
- 使用代码配置cell. 使用
registerClass:forCellWithReuseIdentifier:
或者registerNib:forCellWithReuseIdentifier:
方法来配置cell和重用标识符. 你可能在viewController初始化时调用上面那几个方法. - 使用代码配置supplementary视图. 使用
registerClass:forSupplementaryViewOfKind:WithReuseIdentifier:
或者registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
方法来配置各种view和一个重用标识符进行关联. - cells的重用只需要注册一个重用标识符, supplementary view还需要另一种标识符来标记它,这种标识叫做-kind string. layout对象负责给supplementary view定义这种标识, 比如
UICollectionViewFlowLayout
类提供了两种类型的supplementary view: section header和section footer, 为了标识这两种类型, layout使用了两个常量:UICollectionElementKindSectionHeader
和UICollectionElementkindSectionFooter
. 在布局过程中,layout对象结合kind string和其他布局信息(比如layout attribute),然后collection View传递layout结合的信息给DataSource对象, DataSource使用kind string和reuse identifier来决定使用哪个view. - 标识符注册是个一次性操作,必须在cell重用之前注册完成. Apple不建议你在cell重用过程中重新注册cell的重用标识符.
如果自定义layout对象,那么你需要自己定义supplementary view的kind-string, 每一种supplementary view都有自己的Kind-string
Dequeueing和配置Cells/view
-
你的DataSource对象负责提供cells和补充视图. 在
UICollectionViewDataSource
协议中对应的方法是:collectionView:cellForItemAtIndexPath:
和collectionView:viewForSupplementaryElementOfKind:atIndexPath:
因为cell是CollectionView所必须的,而supplementary view是可选的, 所以上面方法中collectionView:cellForItemAtIndexPath:
是必须的,collectionView:viewForSupplementaryElementOfKind:atIndexPath:
方法是可选的, 它的实现根据你使用的layout对象类型来决定的. 在你实现这两个方法的时候需要遵守的规则是:- 使用
collectionView:cellForItemAtIndexPath:
方法dequeue需要的cell, 用collectionView:viewForSupplementaryElementOfKind:atIndexPath:
方法来dequeue适当类型(type)的补充视图 - 使用特定的indexPath下的data来配置view
- 将配置好的view返回.
- 使用
dequeue过程是用来减轻重复创建view的负担的, 只要你注册过cell和view, 那么dequeue方法不会返回nil. 如果重用队列中没有cell或者view,那么它会创建一个新的cell或者view, 然后将新创建的view返回给你.
dequeue返回的cell应该是维持一个初始的状态, 以便你接下来进行配置. 如果dequeue时, 你的cell必须新建,那么dequeue过程会调用像
initWithFrame:
这样的初始化方法从storyboard/nib文件/类中创建一个全新的cell. 如果不需要你重建, 只需要复用之前的, 那么复用的cell会调用prepareForReuse
方法来重设它的状态, 在自定义cell时可以重写该方法来进行一些特殊的设置, 比如默认值等配置, 或者清理操作.当使用dequeue方法获取了cell后, 那么应该使用indexPath来找到cell对应的data, 然后使用data对象来配置cell, 最后将之返回给collectionView. 代码清单2-2, 展示了如何配置cell.
代码清单2-2 配置自定义cell
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCustomCell* newCell = [self.collectionView dequeueReusableCellWithReuseIdentifier:MyCellID
forIndexPath:indexPath];
newCell.cellLabel.text = [NSString stringWithFormat:@"Section:%d, Item:%d", indexPath.section, indexPath.item];
return newCell;
}
注意:DataSource应该返回一个有效的cell或者其他视图, 如果返回nil, 那么将会崩溃, 因为layout对象要求一个有效的view.
Section和Item的插入,删除,移动
- 对section或者item的删除, 移动, 插入操作需要的步骤:
- 更新DataSource中的数据
- 调用collectionView中的对应的方法来进行相应的操作
- 在更新collectionView之前一定要更新数据, 这个非常重要. 你在调用相应的方法进行对section/itme更新时, collectionView是认为你的数据也已经更新好了, 如果没有更新数据, 在进行collectionView更新时, 从DataSource获取的信息会有误, 当访问的item不存在时, APP会crash
- 当你使用代码操作删除,插入,移动时, collectionView会自动创建一个动画, 如果你同时进行多个更新操作, 需要将多个操作放入
performBatchUpdates:completion:
方法的update-block中执行才会创建动画. 在update-block中可以将insert/delete/move等操作混合进行. - 代码清单2-3展示了如何使用
performBatchUpdates:completion:
来进行合并多个操作. 在更新之前更新DataSource中的data, 全部放入update-blcok中,而且block中的操作时同步的.
代码清单2-3 删除多个item
[self.collectionView performBatchUpdates:^{
NSArray* itemPaths = [self.collectionView indexPathsForSelectedItems];
// Delete the items from the data source.
[self deleteItemsFromDataSourceAtIndexPaths:itemPaths];
// Now delete the items from the collection view.
[self.collectionView deleteItemsAtIndexPaths:itemPaths];
} completion:nil];
管理cell的Selection和Highlight操作
- collectionView默认支持cell的单选, 多选的话需要进行配置. collectionView能够监测到你单击的cell内的事件,然后通过改变cell的highlight(高亮)和select(选中)状态作为回应. 大多数时候,对cell的highlight和select的改变不会改变cell的外观, 除非你给cell的属性
selectedBackgroundView
赋了一个有效的view, 该view在会在cell高亮或者选中的时候显示. - 代码清单2-4中的代码展示了在自定义cell中实现对高亮和选中状态的外观改变. 对cell的
backgroundView
和selectedBackgroundView
分别设置一个view. 当cell选中时, cell的背景色会从红到白的改变.
代码清单2-4 通过设置view的background来展示状态变化
UIView* backgroundView = [[UIView alloc] initWithFrame:self.bounds];
backgroundView.backgroundColor = [UIColor redColor];
self.backgroundView = backgroundView;
UIView* selectedBGView = [[UIView alloc] initWithFrame:self.bounds];
selectedBGView.backgroundColor = [UIColor whiteColor];
self.selectedBackgroundView = selectedBGView;
- collectionView的delegate对象,提供了如下方法来配置cell的highlighted和selected状态:
collectionView:shouldSelectedItemAtIndexPath:
collectionView:shouldDeselectedItemAtIndexPath:
collectionView:didSelectItemAtIndexPath:
collectionView:didDeselectItemAtIndexPath:
collectionView:shouldHighlightItemAtIndexPath:
collectionView:didHighlightItemAtIndexPath:
-
collectionView:didUnhighlightItemAtIndexPath:
这些方法提供了机会让你按需配置cell各状态的外观显示. 举个列子你抛弃使用selectedBackgroundView
属性,你要给cell的选中状态设置了一个自定义的外观. 你可以在collectionView:didSelectItemAtIndexPath:
方法中修改cell的内容, 或者添加一些view, 在collectionView:didDeselectItemAtIndexPath:
方法中移除之前的状态效果, 那么就能在cell非选中状态时恢复正常了. 如果你自己画一个highlight状态, 那么你就需要重写collectionView:didHighlightItemAtIndexPath:
和collectionView:didUnhighlightItemAtIndexPath:
这两个委托方法来实现自己的highlight状态, 如果你同时还是给selectedBackgroundView
赋值了, 那么你应该改变cell的content view来保证效果同时有效. 代码清单2-5展示了改变背景颜色来显示highlight状态的改变
代码清单2-5 给cell添加暂时的highlight效果
- (void)collectionView:(UICollectionView *)colView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell* cell = [colView cellForItemAtIndexPath:indexPath];
cell.contentView.backgroundColor = [UIColor blueColor];
}
- (void)collectionView:(UICollectionView *)colView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell* cell = [colView cellForItemAtIndexPath:indexPath];
cell.contentView.backgroundColor = nil;
}
- cell的highlighted和selected状态间的区别是细微但重要的. highlighted状态是个过渡性的状态, 指当你用手指按住cell时, cell会被高亮显示, 此时cell的
highlighted
属性为YES, 当手指离开屏幕后highlighted
的值会改为NO. 而selected状态的改变只发生在一系列touch事件完成后, 表示这些touch事件是用来选择一个cell的.
图2-3表示用户选择cell发生的一系列事情. 初始touch-down事件导致collectionView将highlighted
设置为YES,如果最终的touch-up事件发生在cell中的话,collectionView会将highlighted
状态改为NO,并将cell的selected
状态设置为YES,此时collectionView会将selectedBackgroundView
显示,这是外观显示唯一改变,如果想进行其他修改可以在delegate对象提供的方法中实现.
- 不管用户选中cell还是取消选中cell,cell的
selected
状态改变总是最后发生. taps发生在cell上总是先改变highlighted状态,只有当tap的一系列事件和高亮动作发生后才会去改变cell的selected的状态, 所以你不要在无意中将两者顺序搞混淆了.
显示cell中的编辑按钮
如果你长按cell的话,collectionView会尝试显示该cell支持的编辑按钮. 这个编辑按钮支持剪切(cut)
,复制(copy)
,粘贴(paste)
三个操作. 一个cell显示编辑按钮需要实现和编辑有关的delegate方法:
-
collectionView:shouldShowMenuForItemAtIndexPath:
,必须返回YES -
collectionView:canPerformAction:forItemAtIndexPath:withSender:
,根据你的需要返回cell支持的操作类型(cut/copy/paste), 支持某个操作就返回YES. -
collectionView:performAction:forItemAtIndexPath:withSender:
,当用户选择某种操作时, 会调用该方法.
代码清单2-6展示了如何通过实现collectionView:canPerformAction:forItemAtIndexPath:withSender:
方法阻止一个cell显示剪切(Cut)
按钮
代码清单2-6 选择性的显示编辑按钮
- (BOOL)collectionView:(UICollectionView *)collectionView
canPerformAction:(SEL)action
forItemAtIndexPath:(NSIndexPath *)indexPath
withSender:(id)sender {
// Support only copying and pasting of cells.
if ([NSStringFromSelector(action) isEqualToString:@"copy:"]
|| [NSStringFromSelector(action) isEqualToString:@"paste:"])
return YES;
// Prevent all other actions.
return NO;
}
如果想知道更多关于粘贴板的命令信息可以参考文档Text Programming Guide for iOS
两个layout间的切换
- layout切换的最简单方法是调用
setCollectionViewLayout:animated:
. 不过你需要控制切换过程或者和切换过程进行交互的话, 就需要用到UICollectionViewTransitionLayout对象 -
UICollectionViewTransitionLayout
类是一种特殊的布局, 当collectionView切换到一个新的布局时, 会是使用该布局. 使用transition layout对象, 你在做切换动画时可以进行非线性, 不同的时间算法, 按照用户事件来移动等操作. 该transition layout对象默认是使用线性切换动画来切换到新的布局. 但你可以继承该类来做一些定制化操作. -
UICollectionViewTransitionLayout
类有提供了一些方法用来追踪布局切换过程. 该对象通过修改transitionProgress
的值来控制切换的进度. 比如transition layout对象和用户手势结合使用, 可以使得该切换过程可以和用户手势交互. 使用updateValue:forAnimatedKey:
和valueForAnimatedKey:
方法来改变和追踪布局对象的某些值的改变, 比如在布局切换过程中使用pinch手势时,你可以使用这两个方法来告诉transition layout对象view间的offset是多少. - 创建
UICollectionViewTransitionLayout
的布局的步骤:- 通过
initWithCurrentLayout:nextLayout:
初始化方法来创建标准的或者自定义的transition layout对象. - 通过
transitionProgress
来修改切换进度, 在切换完成后使用invalidateLayout
方法来取消旧的布局对象 - 实现delegate方法
collectionView:transitionLayoutForOldLayout:newLayout:
, 在方法中返回transition layout对象 - 选择性的调用
updateValue:forAnimatedKey:
方法来更新layout, 稳定值是0.
- 通过