用CollectionView简化代码,专注于业务和UI

UITableView作为高级控件被开发者广泛使用,同样的,UICollectionView由于其NB的布局也被广泛使用。但是后者在使用的时候大多数都属于自定义的比较多,前者则相对普通,list基本上都用。

在业务需求中,常规布局,大多数都是采用UITableView进行的,但是有痛点:

  • 当使在APP内用过一次瀑布流之后,设计师会突然的让你在正常的list中底部追加瀑布流。。虽然是在底部追加,但是要从UITableView迁移到UICollectionView
  • UITableViewDelegateUITableViewDataSource 恐怕每处使用都要写繁琐的相同的代码吧~
  • 很多人也会对其进行高度缓存啊神马的优化策略
  • 不知不觉这些代码堆在一起已经将UIViewController堆的相当的高

然后呢,我们现在使用一个CollectionView将这些操作包装一下,达到这样一个流程:

  1. 自定义Cell、CellModel

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class 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
    }
    }
    }
  2. 请求、处理数据格式化为LabelCell.Model这样的类

  3. 处理好的数据交给CollectionView

    1
    list.sections = model.format(...)
  4. 休息一会,完工了~

然后,来看看Collection里面都做了什么操作

CollectionView

为了达到上面效果中的第三步,我们需要自定义一个CollectionView来处理刷新数据、设置代理、设置数据源、注册cell、等操作。

需要说明的是,此处使用CHTCollectionViewWaterfallLayout来处理瀑布流。

创建一个CollectionViewSection的类/结构体来保存section信息。比如说:sectionInsetminimumColumnSpacingminimumInteritemSpacingcolumnCount、等

CollectionView中设置一个var sections = [KTJCollectionViewSection]()用于保存sections

实现UICollectionViewDataSourceUICollectionViewDelegate。此处省略约1千字…

ListItemDefaultProtocol:绑定Cell和Model

上面省略约1千字中有一个并没有说:CollectionViewSectionheaderitemsfooter如何实现。😂

好了,此处这三个均采用协议ListItemDefaultProtocol来实现。(PS:大家都喜欢用父类,但父类只有一个,为了兼容和冲突,这里协议是最好不过的~,特别是swift中的协议)

协议需要给出这么几个信息:

  • reuseIdentifier 用来复用的重用标识符
  • cellClass 用来注册用的类名
  • func fillModel(item: AnyObject) 用来给cell填充model的方法

那么这个协议就出来了:

1
2
3
4
5
protocol KTJListItemProtocol {
var identifa: String { get }
var registClass: AnyClass { get }
func fillModel(item: AnyObject)
}

swift协议支持默认实现,于是乎,就可以再写一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol ListItemDefaultProtocol: KTJListItemProtocol {
associatedtype ItemType: UIView
func fillModel(view: ItemType)
}
extension ListItemDefaultProtocol {
var identifa: String { return NSStringFromClass(ItemType.self)}
var registClass: AnyClass { return ItemType.self }
func fillModel(item: AnyObject) {
if let reusableView = item as? ItemType {
fillModel(view: reusableView)
}
}
}

然后,就可以愉快的玩耍了~~

最后Cell中的的代码效果就是这个样子
再加上数据填充是不是很简单了呢~~

以后如何开发

真的如同前面的例子一样,只需要这么几步绕不过去的:

  1. 处理接口吐出来的数据。
  2. 创建新的UI样式,并做好接口数据中间件。
  3. 点击事件在处理数据的时候预先埋好,所有的数据、逻辑和UI数据一起被传递,不需要多次类型判断。

统一使用CollectionView还有一个好处:不管前面谁写的一个UI,都能拉过来用。不用做中间层去从TableViewCellCollectionViewCell

开发只需要关心业务,业务。安安心心做一个写业务的程序员吧~

关于Cell的跳转处理

乍一看,完美了。不过还有一个巨烦的跳转处理。。。不过,莫怕

跳转,有这么几种:

  • 支持路由的URL跳转,这个是最简单的,也是最爽的
  • 只能复杂的进行创建类、赋值、push啊神马的

由于我们将数据全部处理好后扔给了cellmodel,而跳转、打点所需要的参数均在此处为最全的,我们可以将回调作为数据一起传递。。

嗯, 事件需要传递的对象大约这个样子:

1
2
3
4
5
6
7
8
9
10
struct Jmp {
/// 支持URL跳转的URL
let url: String?
/// 打点事件名
let event: String?
/// 打点参数
let attr: [AnyHashable: Any]?
/// 不支持URL的直接回调
let eventCallback: (() -> Void)?
}

然后cell对应的另一个类是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
class KTJJmp: NSObject {
var info: Jmp?
let action = #selector(topVCJmp)
@objc func topVCJmp() {
... // 打点
KTJURLJump.jumpToViewOnTopVC(jmp: info)
info?.eventCallback?()
}
func tap() -> UITapGestureRecognizer {
return UITapGestureRecognizer(target: self, action: #selector(topVCJmp))
}
}

两个类共同作用就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
class JmpCell: UICollectionViewCell {
let jmp = JmpCellModel()
func setup() {
...
jmpBtn.addTarget(jmp, action: jmp.action, for: .touchUpInside)
}
}
class JmpCellModel: ListItemDefaultProtocol {
var jmp: Jmp?
func fillModel(view: JmpCell) {
view.jmp.info = jmp
}
}

至于这个JmpCellModel.jmp怎么生成,这里就不说了。

关于算高那点事

算高位置的选择

我也曾纠结过frame的代码应该写在哪里。。有写在初始化的,有单独写一个方法的,还有写在func sizeThatFits(:) -> CGSize

由于当年对UITableView-FDTemplateLayoutCell中毒较深,所以沿用了最后一种方案。

算高使用的view的选择

在算高的时候,首先需要明确的是:高度是数据和当前最大宽度共同决定的,所以在算高的时候需要拿着数据、view然后才能去算高(PS:至于那些拿着数据硬算出一个高度的代码,此处不发表看法😂,早晚会后悔的)

然后就是UITableViewUICollectionView的算高是通过一个单独的代理去获取的,并不提供view去计算。。这就是矛盾的地方。

虽然UITableViewUICollectionView在算高的地方不提供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.swiftfunc collectionView(:,layout:,sizeForItemAt:) -> CGSize函数中。

关于代理那点事

丫的,需求总是比想的多,但是办法也比问题多。

对于CollectionView来说,代理都设置成了self,这样会导致有时候需要UIScrollViewDelegate干些事情的时候总是不那么自由。。。实乃新痛点,不过有办法。

下面是几种方案:

KVO

直接KVO到self.contentOffset,这样一了百了。写什么代理,要什么自行车!(PS:swift提供闭包的代理,也是很好用的)

如果不会,请自行GoogleObjC-中国: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
2
3
4
5
6
7
8
9
protocol CollectionViewDelegate: NSObjectProtocol {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
}
class CollectionView {
weak var allDelegate: CollectionViewDelegate?
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.allDelegate?.collectionView(self, didSelectItemAt: indexPath)
}

这方案没啥聊的。。

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部分没有写跳转相关内容,但是别的都有了😂

别的

好像没了吧~,想起来了再补充咯

偶然看到一个类似的文章

Table View 太複雜?利用 MVVM 和 Protocol 就可以為它重構瘦身!

昨天(2018.7.24)翻文章的时候看到这个文章,然后将文章和代码捋了一下,发现这才是MVVM吧。觉得不错,贴出来~