戏说CollectionView、TableView如何优雅应对变化的界面展示

前言

App里很多页面都是用UICollectionView和UITableView做的,比如一些列表类的、详情类的,甚至是商城首页等等,只要是分块的,特别是根据数据源不同而展示不同的页面,都很适合用,但怎么用,使它容易修改、扩展,是个问题。

产品经理老李说

  1. 这一块功能有数据的时候就展示,没有就隐藏
  2. 这一块挪到那块下面
  3. 这一块XXX功能删了,现在不需要了
  4. 之前删的XXX功能补回来吧,然后改成放在XXX功能块下面
  5. 这块地方的高度根据内容动态设置
  6. ……(数不尽的小数点)

程序猿懵了

  1. 咦,我之前这个Section放的是什么,看界面的话不准确,有些隐藏之类的情况,界面上这块东西对应的是哪个Cell,也不确定了。
  2. Delegate和DataSource,少的时候只实现三四个方法,多的时候实现七八个,这些都和Section、Cell的各项信息有关,这时候做什么变动都需要改动多处地方,if…else…也就写得到处都是。
  3. ……(数不尽的小数点)

这些麻烦不难解决,就是有时候会很花时间,效率低,于是我想了一个方法解决它。

最初是在做某一类详情的时候想出来的,后面经过应用到其他页面和几次变更的实践之后发现,还真挺方便的!下面将举几个例子来说明这个方法及其特点。

本想将本文语言组织得有趣些,但可惜功力不够,时间也紧哈,下次再尝试。

例子一

产品经理老李说:“给我实现xxx功能,有5部分信息,其中A、C、D功能没有东西的时候是隐藏的……”

程序猿答道:“哦……”

转化成技术需求

有五种Cell,分别是ACell(根据data.a判断隐藏与否)、BCell(默认有)、CCell(根据data.c数组个数判断隐藏与否,并且根据个数设置cell大小)、DCell(根据data.d数组个数判断隐藏与否)、ECell(默认有),隐藏与否顺序都是A\B\C\D\E。

笨拙的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class XXXController: UICollectionViewController {

let data = ......
override func viewDidLoad() {

super.viewDidLoad()
collectionView.reloadData()
}
}

extension XXXController: UICollectionViewDelegateFlowLayout {

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {

// switch 或 if...else...
switch indexPath.row {
case 0:
// 判断是A还是B
case 1:
// 判断是B还是C还是D还是E......(心里骂着产品经理)
// 这里很容易犯错,细想下你就知道了......(又被扣工资了囧)
case 2:
// 判断是C还是D还是E......(心里骂着产品经理)
case 3:
// 判断是D还是E......(心里骂着产品经理)
case 4:
// ECell
default:
// 还要写default......
}
}
}

extension XXXController {// UICollectionViewDelegate、UICollectionViewDataSource

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// 根据数据源计算分别有没有A/C/D
// 一堆if...else......(心里骂着产品经理)
return 2 + ......
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// 同sizeForItemAtIndexPath(心里骂着产品经理)
}

override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// 同sizeForItemAtIndexPath(心里骂着产品经理)
}
}

优雅的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
enum XXXItemTypes {
case A
case B
case C(count:Int)
case D
case E
......
}

class XXXController: UICollectionViewController {

let data = ......
var itemTypes:[XXXItemTypes] = []
override func viewDidLoad() {

super.viewDidLoad()
configItemTypes(data)
collectionView.reloadData()
}
}

private extension XXXController {
func configItemTypes(data) {

itemTypes = []
if let _ = data.a {// 根据对象是否存在判断隐藏与否的情况
itemTypes.append(XXXItemTypes.A)
}
itemTypes.append(XXXItemTypes.B)// 默认有的情况
if let cCount = data.c.count where cCount > 0 {// 根据个数判断隐藏与否的情况
itemTypes.append(XXXItemTypes.C(cCount))
}
if let dCount = data.d.count where dCount > 0 {
itemTypes.append(XXXItemTypes.D)
}
itemTypes.append(XXXItemTypes.E)// 默认有的情况
......
}
}

extension XXXController: UICollectionViewDelegateFlowLayout {

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {

switch itemTypes[indexPath.row] {
case .A:
return ACell.cellSize(......)
case .B(let count):
return BCell.cellSize(count: count)
case .C:
return CCell.cellSize(......)
case .D:
return DCell.cellSize(......)
case .E:
return ECell.cellSize(......)
}
}
}

extension XXXController {// UICollectionViewDelegate、UICollectionViewDataSource

override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return itemTypes.count
}

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

switch itemTypes[indexPath.row] {
case .A:
......
case .B:
......
case .C:
......
case .D:
......
case .E:
......
}
}

override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
switch itemTypes[indexPath.row] {
......
}
}
}

小分析

可以看到,所有的判断(4处地方)都归结一处,也就是configItemTypes里面,省去了一堆因为隐藏与否而做的判断逻辑,而且不会出现复杂的判断的逻辑出错。

例子二

又是产品经理老李说:“D功能很重要,放在第一处吧,这很容易实现吧!”

程序猿答道:“嗯……”

笨拙的实现

每个UICollectionViewDelegate、UICollectionViewDataSource、UICollectionViewDelegateFlowLayout的方法里面的判断逻辑都修改……

磨刀霍霍向老李

优雅的实现

只需修改configItemTypes,将DCell的判断逻辑放到最前面先判断。(这是真的吗??就这么简单??)

小分析

任何顺序变化都可以通过简单修改configItemTypes而实现,不改动其他地方。

例子三

老李又来了:“把C功能去掉……”

程序猿答道:“嗯……”

笨拙的实现

同例子二,又要重新看那段判断逻辑。

优雅的实现

同样只需修改configItemTypes,将添加CCell的部分删除。(好崇拜自己啊!!!!!!!!)

小分析

任何增加和删除都可以通过简单修改configItemTypes而实现,不改动其他地方。

进阶例子

上面的几个例子都是没有Section的,如果再加多一层Section,那么复杂的程度就上升很多了,这里就不再赘述了。主要说下这种情况下枚举的数据结构设计,可以有很多种,下面是我的一个实现,还可以更优化。

1
2
3
4
5
6
7
8
9
10
11
enum XXXSectionType {
case SectionA(rowTypes:[XXXRowType])
case SectionB(rowTypes:[XXXRowType], index: Int)
case SectionC(rowTypes:[XXXRowType])
}

enum XXXRowType {
case RowA
case RowB
case RowC
}

这种枚举的方法在有section的情况下更彰显了它的价值!依着上面的几个例子的需求尝试下就能体会得出来了。

总结

从上面的几个需求和处理可以慢慢抽象出这麻烦的问题所在:
=>Section/Cell的分布情况没有统一
=>导致Delegate和DataSource的多处方法需要进行判断
=>导致展示变化时Delegate和DataSource的多处方法都要修改
=>导致修改辨识麻烦
=>导致效率低

枚举的方法将所有分布情况都放在了分布数组的初始化上,Delegate/DataSource不需要通过indexPath去判断自己当前是哪个row/item,实际上就是将未知的麻烦的indexPath抽象出一维数组或者二维数组来表示它们的分布,并且每个分布情况所需的设置(如Cell高度、Cell样式、Header高度等等)都是固定的,所以任何变动只需修改那个分布数组。

看似简单的抽取,却处理掉了很多麻烦的问题和冗余的处理!

-END-
欢迎到我的博客交流:https://zackzheng.info