用CollectionView简化代码,专注于业务和UI
UITableView
作为高级控件被开发者广泛使用,同样的,UICollectionView
由于其NB的布局也被广泛使用。但是后者在使用的时候大多数都属于自定义的比较多,前者则相对普通,list基本上都用。
在业务需求中,常规布局,大多数都是采用UITableView
进行的,但是有痛点:
- 当使在APP内用过一次瀑布流之后,设计师会突然的让你在正常的list中底部追加瀑布流。。虽然是在底部追加,但是要从
UITableView
迁移到UICollectionView
UITableViewDelegate
、UITableViewDataSource
恐怕每处使用都要写繁琐的相同的代码吧~- 很多人也会对其进行高度缓存啊神马的优化策略
- 不知不觉这些代码堆在一起已经将
UIViewController
堆的相当的高
然后呢,我们现在使用一个CollectionView
将这些操作包装一下,达到这样一个流程:
自定义Cell、CellModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class LabelCell: UICollectionViewCell {
let info = UILabel()
...
override func sizeThatFits(_ size: CGSize) -> CGSize {
...
return CGSize(width: size.width, height: info.frame.maxY + info.frame.minY)
}
}
extension LabelCell {
class Model: ListItemDefaultProtocol {
var info: String?
func fillModel(view: LabelCell) {
view.info.text = info
}
}
}请求、处理数据格式化为
LabelCell.Model
这样的类处理好的数据交给
CollectionView
1
list.sections = model.format(...)
休息一会,完工了~
然后,来看看Collection
里面都做了什么操作
CollectionView
为了达到上面效果中的第三步,我们需要自定义一个CollectionView
来处理刷新数据、设置代理、设置数据源、注册cell、等操作。
需要说明的是,此处使用CHTCollectionViewWaterfallLayout
来处理瀑布流。
创建一个CollectionViewSection
的类/结构体来保存section信息。比如说:sectionInset
、minimumColumnSpacing
、minimumInteritemSpacing
、columnCount
、等
在CollectionView
中设置一个var sections = [KTJCollectionViewSection]()
用于保存sections
实现UICollectionViewDataSource
、 UICollectionViewDelegate
。此处省略约1千字…
ListItemDefaultProtocol:绑定Cell和Model
上面省略约1千字
中有一个并没有说:CollectionViewSection
的header
、items
、footer
如何实现。😂
好了,此处这三个均采用协议ListItemDefaultProtocol
来实现。(PS:大家都喜欢用父类,但父类只有一个,为了兼容和冲突,这里协议是最好不过的~,特别是swift中的协议)
协议需要给出这么几个信息:
reuseIdentifier
用来复用的重用标识符cellClass
用来注册用的类名func fillModel(item: AnyObject)
用来给cell填充model的方法
那么这个协议就出来了:
1 | protocol KTJListItemProtocol { |
在swift协议支持默认实现,于是乎,就可以再写一个:
1 | protocol ListItemDefaultProtocol: KTJListItemProtocol { |
然后,就可以愉快的玩耍了~~
最后Cell中的的代码效果就是这个样子。
再加上数据填充是不是很简单了呢~~
以后如何开发
真的如同前面的例子一样,只需要这么几步绕不过去的:
- 处理接口吐出来的数据。
- 创建新的UI样式,并做好接口数据中间件。
- 点击事件在处理数据的时候预先埋好,所有的数据、逻辑和UI数据一起被传递,不需要多次类型判断。
统一使用CollectionView
还有一个好处:不管前面谁写的一个UI,都能拉过来用。不用做中间层去从TableViewCell
转CollectionViewCell
。
开发只需要关心业务,业务。安安心心做一个写业务的程序员吧~
关于Cell的跳转处理
乍一看,完美了。不过还有一个巨烦的跳转处理。。。不过,莫怕
跳转,有这么几种:
- 支持路由的URL跳转,这个是最简单的,也是最爽的
- 只能复杂的进行创建类、赋值、push啊神马的
由于我们将数据全部处理好后扔给了cellmodel
,而跳转、打点所需要的参数均在此处为最全的,我们可以将回调作为数据一起传递。。
嗯, 事件需要传递的对象大约这个样子:
1 | struct Jmp { |
然后cell对应的另一个类是这样的:
1 | class KTJJmp: NSObject { |
两个类共同作用就是:
1 | class JmpCell: UICollectionViewCell { |
至于这个JmpCellModel.jmp
怎么生成,这里就不说了。
关于算高那点事
算高位置的选择
我也曾纠结过frame的代码应该写在哪里。。有写在初始化的,有单独写一个方法的,还有写在func sizeThatFits(:) -> CGSize
由于当年对UITableView-FDTemplateLayoutCell中毒较深,所以沿用了最后一种方案。
算高使用的view的选择
在算高的时候,首先需要明确的是:高度是数据和当前最大宽度共同决定的,所以在算高的时候需要拿着数据、view然后才能去算高(PS:至于那些拿着数据硬算出一个高度的代码,此处不发表看法😂,早晚会后悔的)
然后就是UITableView
、UICollectionView
的算高是通过一个单独的代理去获取的,并不提供view去计算。。这就是矛盾的地方。
虽然UITableView
、UICollectionView
在算高的地方不提供View,但是有一个dequeueReusableCell....
的方法可以获取到缓存池中的Cell
呀
如果你这样做了,那么你将会付出惨重的代码。这个方案我在博客园2015.03.17的“iOS 优化性能之TableView”中已经说了:
dequeueReusableCellWithIdentifier:此函数的调用要注意以下几点:
i.此函数的返回值是做为tableView:cellForRowAtIndexPath:的返回值的。这样保证拿出来的完整还给TableView。
ii.如果此函数的返回值不是为了给tableView:cellForRowAtIndexPath:做返回值,那么你要注意这是在一个拿取别人稀缺资源的操作,需要注意珍惜这个返回值,能不浪费就不要浪费。
iii.对于AL自动适应的TableView取Cell时候要注意保存。个人建议封装TableView,然后用来计算高度的Cell保存在TableView中。对于多种类型的Cell,则可以使用复用标识符作为Key的字典来存储。这样能够有效节约dequeueReusableCellWithIdentifier:调用次数。
UICollectionView
也是如此。所以使用dequeueReusableCell....
方法是很不靠谱的。
在UITableView-FDTemplateLayoutCell中使用了字典保存缓存算高的View,也是比较赞的。
但是现在有了CollectionView
,我们可以不用OC的objc_setAssociatedObject(,,,)
去处理而直接使用了NSCache
去处理就行了~
如果看了CollectionView
就会发现,ListItemProtocol
还有一个get方法var newItem: AnyObject { get }
这个是用来算高的。
在CollectionView.swift关于高度的代理设置中可以看到相关获取和设置。
算高的时候变局的控制
高度的计算现在已经可以正常计算,但是对于columnCount > 1
的时候,牵扯到间隙的计算需要注意一次啊。毕竟CHTCollectionViewWaterfallLayout
功能更强大。
相关代码在CollectionView.swift的func collectionView(:,layout:,sizeForItemAt:) -> CGSize
函数中。
关于代理那点事
丫的,需求总是比想的多,但是办法也比问题多。
对于CollectionView
来说,代理都设置成了self,这样会导致有时候需要UIScrollViewDelegate
干些事情的时候总是不那么自由。。。实乃新痛点,不过有办法。
下面是几种方案:
KVO
直接KVO到self.contentOffset
,这样一了百了。写什么代理,要什么自行车!(PS:swift提供闭包的代理,也是很好用的)
如果不会,请自行Google。ObjC-中国:KVC 和 KVO,我只能帮你到这里
Rx系
很抱歉,你不能用self.rx.contentOffset
。原因是CHTCollectionViewWaterfallLayout
中有这么一行断言:
1 NSAssert([self.delegate conformsToProtocol:@protocol(CHTCollectionViewDelegateWaterfallLayout)], @"UICollectionView's delegate should conform to CHTCollectionViewDelegateWaterfallLayout protocol");
但是,你可以使用Rx里面的KVO呀~也是贼方便
RAC系
抱歉,我不会。原因在此准备食用RAC(ReactiveCocoa)的顾虑
代理传递
当然也可以使用代理进行传递出去。大致这个样子:
1 | protocol CollectionViewDelegate: NSObjectProtocol { |
这方案没啥聊的。。
OC咋办?
这个OC也能用,只不过需要一些手段,比如说:继承、扩展、等
ListItemProtocol
就是为了给OC使用留的,还有就是CollectionViewSection
使用了类,而不是结构体。
不过具体没有例子,不想写OC代码,太麻烦了。。主要是这种思路:)
填充Cell的数据为什么会在CellModel中?
对于Cell来说,Cell是干净的,没有任何继承、协议来限制Cell如何处理。那么Cell就可以接受来自任何模块的各种风格的Cell,这是我想要的减少侵入。
cellModel中定义了Cell所对应的填充数据格式,在第一次写的时候难免会按照业务逻辑起一些业务逻辑的名字,在复用的时候又不能改名字,会对维护和开发加了一定量的复杂度,这不是我想要的。。
于是乎每个CellModel根据自己的需要绑定需要绑定的Cell,去填充Cell所对应的数据就好。只需要侵入cellModel即可。代价最小,受益最大。
PS.对于同一个List,需要注意cell的复用和多个cellModel同时绑定一个cell时,属性的设置。
Demo
Demo部分没有写跳转相关内容,但是别的都有了😂
别的
好像没了吧~,想起来了再补充咯
偶然看到一个类似的文章
昨天(2018.7.24)翻文章的时候看到这个文章,然后将文章和代码捋了一下,发现这才是MVVM吧。觉得不错,贴出来~