关于 iOS 中 View 和 Cell 的思考

Cell 组件

在 iOS 开发中,当我们需要实现列表需求时,通常会写出 Cell 组件来实现每个列表项的布局和样式。但是这种方式的缺点是,当列表的实现方式从 UITableView 切换到 UICollectionView 时,我们的 Cell 组件就需要做出一些修改,这不够灵活。甚至当我们需要在其他地方使用 Cell 组件时,还需要重新实现一遍,这对于复用和维护来说都不是最佳的解决方案。

当使用 Cell 的方式实现下面的课程组件:

列表组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义课程的 Cell 组件
final class CourseCell: UITableCell {
// ...

let coverView = CourseCoverView()
let infoView = CourseInfoView()
}

// 定义课程的封面组件
final class CourseCoverView: UIView {
// ...
}

// 定义课程的详细信息组件
final class CourseInfoView: UIView {
// ...
}

如果现在又来一个新的需求,需要在列表的最上面放一排可以横向滚动的封面图,下面依旧还是课程列表。这里使用 UICollectionView 来实现我们的这个需求,那么需要修改的代码如下:

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
// 定义课程的 Cell 组件
final class CourseCell: UICollectionViewCell {
// ...

let coverView = CourseCoverView()
let infoView = CourseInfoView()
}

// 定义课程封面的 Cell 组件
final class CourseCoverCell: UICollectionViewCell {
// ...

let coverView = CourseCoverView()
}

// 定义课程的封面组件
final class CourseCoverView: UIView {
// ...
}

// 定义课程的详细信息组件
final class CourseInfoView: UIView {
// ...
}

View 组件

因此,使用 View 组件实现列表项会更加灵活。在这种情况下,Cell 的作用只是一个容器,只是把 View 放到 Cell 上去,并且负责做一些注册和重用的机制。但这种方式的问题是,每当我们把这个 View 用在 UITableView 或 UICollectionView 上时,我们就必须编写对应的 Cell 容器代码,这部分代码的工作基本相同(添加 View,添加约束)。

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
// MARK: - Cell 组件
// 定义课程的 UICollectionViewCell 组件
final class CourseCollectionCell: UICollectionViewCell {
// ...

let courseView = CourseView()
}

// 定义课程的 UITableViewCell 组件
final class CourseTableCell: UITableViewCell {
// ...

let courseView = CourseView()
}

// MARK: - View 组件
// 定义课程组件
final class CourseView: UIView {
// ...

let coverView = CourseCoverView()
let infoView = CourseInfoView()
}

// 定义课程的封面组件
final class CourseCoverView: UIView {
// ...
}

// 定义课程的详细信息组件
final class CourseInfoView: UIView {
// ...
}

我们可以从上面的示例代码中看出,这种方式在实际的工程中依旧会出现大量的模板代码,所这就需要我们思考有没有什么方式可以来避免编写大量、重复的模板代码了?

CheapCell

CheapCell 是之前的项目重构成 Swift 之后实现的一套能够让你不用写 Cell 容器的库,这个库当时帮助我们减少了上千行的 Cell 容器代码。它的核心代码很简单,最重要的是利用了 Swift 的面相协议编程的方式来实现的,能够极大地减少代码量和提高代码的复用性和可维护性。具体的实现代码可以看我的 Github 仓库 https://github.com/HParis/CheapCell。

下面是集成了 CheapCell 之后的使用方式:

第一步:让我们的 View 组件遵循我们定义的 CheapCell 协议

1
2
3
// MARK: - CheapCell 协议
extension CourseView: CheapCell {}
extension CourseCoverView: CheapCell {}

第二步:在 UICollectionView 中的使用方式(UITableView 的使用方式也是类似的)

1
2
3
4
5
6
7
// Register cheap cell
collectionView.registerCheapCell(CourseView.self)
collectionView.registerCheapCell(CourseCoverView.self)

// Reuse cheep cell
let cell = collectionView.dequeueReusableCheapCell(for: indexPath) as CollectionCell<CourseView>
let cell = collectionView.dequeueReusableCheapCell(for: indexPath) as CollectionCell<CourseCoverView>

配合第三方库使用

如果你使用了一些第三方的 Cell 的话,比如 SwipeCellKit,那么你可以按照下面的方式来实现你的 CheapCell:

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
import SwipeCellKit

public extension UICollectionView {
func registerCheapSwipeCell<T: CheapCell>(_ : T.Type) {
register(CollectionSwipeCell<T>.self, forCellWithReuseIdentifier: CollectionSwipeCell<T>.identifier)
}

func dequeueReusableCheapSwaipeCell<T: CheapCell>(for indexPath: IndexPath) -> CollectionSwipeCell<T> {
dequeueReusableCell(withReuseIdentifier: CollectionSwipeCell<T>.identifier, for: indexPath) as! CollectionSwipeCell<T>
}
}


public final class CollectionSwipeCell<T: CheapCell>: SwipeCollectionViewCell {
public static var identifier: String {
return T.identifier
}

public let itemView = T()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}

private func setupView() {
contentView.addSubview(itemView)

itemView.translatesAutoresizingMaskIntoConstraints = false
itemView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
itemView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
itemView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
itemView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
}

现在你就可以直接通过 CheapCell 的方式来使用了:

1
2
3
4
5
6
// Register cheap swipe cell
collectionView.registerCheapSwipeCell(CourseView.self)

// Reuse cheep swipe cell
let cell = collectionView.dequeueReusableCheapCell(for: indexPath) as CollectionSwipeCell<CourseView>
cell.delegate = self