iOS Collection View 编程指导(二)-DataSource和Delegate的设计

每个CollectionView都需要一个DataSource对象. DataSource对象是内容提供者. 它可以是应用程序的数据模型中的对象,也可以是管理集合视图的视图控制器。数据源的唯一要求是它必须能够提供集合视图需要的内容信息,例如有多少item,以及显示这些项时要使用哪些view。

Delegate对象用来管理内容展现和交互. 虽然delegate的主要工作是管理突出显示和选择cell,但是可以扩展它以提供附加信息. 例如,flow layout扩展了delegate基本行为来定制布局度量,例如单元格的大小和它们之间的间隔.

DataSource管理内容信息


datasource对象用来管理CollectionView中显示的内容, 他必须遵循UICollectionViewDataSource协议, 该协议定义了你需要实现的一些基本行为和方法. DataSource的工作就是需要回答CollectionView如下的问题:

  • CollectionView中包含多少个section
  • 每一个section中包含的item有多少.
  • 用什么view来展示item中的内容.

sectionitem是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只支持sectionitem两个层级, 这个和UITableView类似. Supplementarydecoration的index path更加复杂. 如果某个item的索引路径层级>1, 那索引路径的第一个值一般指该item属于哪个section(组)。传统上,只有第二个索引是必要的,但补充和装饰视图并不局限于只有两个。在设计数据源时要记住这一点。

不管你在DataSource对象中如何组织sectionitem,最终的显示还是要靠layout对象. 不同的布局对于显示sectionitem是不一样的, 如下图所示,使用flowLayout的会将section在竖直方向连续排列,而自定义的布局对象的话,section的排列是非线性的,这个取决于你使用的layout对象.

图2-1section的排列由layout对象控制

设计你的数据对象

一个有效的data source对象使用sectionitem来帮助管理底层数据对象. 将你的数据组织成sectionitem将有助于你实现data source中的方法. 因为DataSource方法将会频繁调用,所以你实现的DataSource方法应该能快地速获取数据.

一个简单但不唯一的方法是将你的数据保存在多维数组中,如下图所示. 多维数组的第一层对应section,一个section包含一个array,一个item对应sectionArray中的某一个元素.

图2-2使用二维数据来保存数据

当你设计数据结构时, 你可以从数组入手,再进行更进一步的设计. 通常,你设计的数据结构不应该成为性能的瓶颈. 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视图, 你的代码必须实现:

  1. 在storyboard中插入cell和view(可选地,为每种支持的单元格或视图注册一个类或NIB文件。)
  2. 在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使用了两个常量:UICollectionElementKindSectionHeaderUICollectionElementkindSectionFooter. 在布局过程中,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对象类型来决定的. 在你实现这两个方法的时候需要遵守的规则是:

    1. 使用collectionView:cellForItemAtIndexPath:方法dequeue需要的cell, 用collectionView:viewForSupplementaryElementOfKind:atIndexPath:方法来dequeue适当类型(type)的补充视图
    2. 使用特定的indexPath下的data来配置view
    3. 将配置好的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的backgroundViewselectedBackgroundView分别设置一个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对象提供的方法中实现.

图2-3 cell中的touch详解
  • 不管用户选中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.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容