【XE2V 项目收获系列】一、YLExtensions:让 UI
文章首发于掘金:https://juejin.im/post/6862954764179603469
前言
XE2V 是一个 V2EX 客户端,作为我的第一个项目,我真切的希望能把它写好。这愿望看起来如此普通,但开始之后才发现,写出让自己满意的代码远没有看起来那么简单,以至于直到现在项目还处于未完成的状态。
由于经验的匮乏及自身的愚钝,许多对一般开发者手到擒来的事情对我来说都成了大问题。不了解的东西太多了,而我应对困难的方法,呃,能避则避。于是,拖延成了常态。但项目总是要完成的,我又不得不在某个时间继续。重新拾起的项目总是左看右看不顺眼,着实面目可憎,心一横,就把项目推倒重来了。于是,时间成了拖延与重写的无尽循环。幸运的是,在项目的一次次重写中,一些问题终究是被解决了,我把它们提取出来做成库,与大家分享。水平所限,定然有诸多不足,请多指教。
问题的提出
当一个 UITableView 或 UICollectionView 页面包含多个种类的 cell 时,注册及配置这些 cell 需要写很多重复的代码,譬如,一个 table view 页面包含了四类 cell:ACell、BCell、CCell 和 DCell,在 tableView(_:cellForRowAt:)
方法中,我们可能这样写:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "ACell", for: indexPath) as! ACell
cell.configure(model[indexPath.section][indexPath.row])
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: "BCell", for: indexPath) as! BCell
cell.configure(model[indexPath.section][indexPath.row])
return cell
case 2:
let cell = tableView.dequeueReusableCell(withIdentifier: "CCell", for: indexPath) as! CCell
cell.configure(model[indexPath.section][indexPath.row])
return cell
case 3:
let cell = tableView.dequeueReusableCell(withIdentifier: "DCell", for: indexPath) as! DCell
cell.configure(model[indexPath.section][indexPath.row])
return cell
}
}
一种模式重复四遍,实在不够优雅。理想中,tableView(_:cellForRowAt:)
方法应该类似这样:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(...)
cell.configure(data[indexPath.section][indexPath.row])
return cell
}
那么,能否找到一种方法实现上面的效果呢?
简化 tableView(_:cellForRowAt:)
方法
首先,我们要使得 dequeueReusableCell(…)
方法能够在不同的 Identifier
下返回不同的 cell。如何做到?不透明类型正是用来解决这类问题的。为此,我们给 UITableView 添加一个扩展:
extension UITableView {
func dequeueReusableCell(
for indexPath: IndexPath,
with identifiers: [String]
) -> some UITableViewCell {
for (index, identifier) in identifiers.enumerated() where index == indexPath.section {
let cell = dequeueReusableCell(withIdentifier: identifier, for: indexPath)
return cell
}
fatalError()
}
}
接下来,每类 cell 都要有一个 configure(_:)
方法,这容易完成,扩展一下 UITableViewCell 即可:
@objc protocol Configurable {
func configure(_ model: Any?)
}
extension UITableViewCell: Configurable {
func configure(_ model: Any?) { }
}
于是,tableView(_:cellForRowAt:)
方法中我们就可以这样写:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(for: indexPath, with: ["ACell", "BCell", "CCell", "DCell"])
cell.configure(model[indexPath.section][indexPath.row])
return cell
}
表示 Identifier
的更好方式
字符串容易出现拼写错误,有没有更好地方式表示 Identifier
呢?一种解决方式是给 cell 添加一个 identifier
属性,这样我们就可以利用 Xcode 的自动补全功能帮助我们避免错误。我们可以这样做:
protocol ReusableView { }
extension ReusableView {
static var reuseIdentifier: String {
return String(describing: self)
}
}
extension UITableViewCell: ReusableView { }
然后,在 tableView(_:cellForRowAt:)
方法中使用:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(for: indexPath, with: [ACell.reuseIdentifier, BCell.reuseIdentifier, CCell.reuseIdentifier, DCell.reuseIdentifier])
cell.configure(model[indexPath.section][indexPath.row])
return cell
}
简化 cell 的注册
另一个出现重复代码的地方是 cell 注册时,比如,当 A、B、C、D 四类 cell 由纯代码方式创建时,我们会这样注册:
tableView.register(ACell.self, forCellReuseIdentifier: ACell.reuseIdentifier)
tableView.register(BCell.self, forCellReuseIdentifier: BCell.reuseIdentifier)
tableView.register(CCell.self, forCellReuseIdentifier: CCell.reuseIdentifier)
tableView.register(DCell.self, forCellReuseIdentifier: DCell.reuseIdentifier)
能否简化注册过程?
仔细观察注册方法,其实只需要提供一个 UITableViewCell.Type 类型的参数即可。基于此,我们可以给 UITableView 添加一个这样的扩展:
extension UITableView {
func registerCells(with cells: [UITableViewCell.Type]) {
for cell in cells {
register(cell, forCellReuseIdentifier: cell.reuseIdentifier)
}
}
}
从而,在注册时只需写一行代码:
tableView.registerCells(with: [ACell.self, BCell.self, CCell.self, DCell.self])
如果 cell 是用 nib 方式创建的呢?这也简单。我们先扩展一下 UITableViewCell:
protocol NibView { }
extension NibView where Self: UIView {
static var nib: UINib {
return UINib(nibName: String(describing: self), bundle: nil)
}
}
extension UITableViewCell: NibView { }
再给 UITableView 添加一个扩展:
extension UITableView {
func registerNibs(with cells: [UITableViewCell.Type]) {
for cell in cells {
register(cell.nib, forCellReuseIdentifier: cell.reuseIdentifier)
}
}
}
然后我们就可以用类似的方式注册 nib 方式创建的 cell 了。
如此,问题就都得到了解决。不过,审视一下 dequeueReusableCell(for:with:)
方法和 registerCells(with:)
方法,它们的参数感觉,呃,不大漂亮。有没有更好地表示方式?嗯,我们可以把它们放入 table view 的 model 的属性中,使用时调用一下就行了。
给 Model 下一个定义
啊,model!说了这么久,我们还没有考虑过它。什么是 model?你可能会说它是一个提供数据的东西。确实,我们一般都是把 model 当作数据提供者使用。不过,对于 UITableView 的 model,它其实可以承载更多。每个 table view 都有一个 model 和若干种类的 cell,于是,model 和 cell 间可以建立起联系,我们可以把 cell 的类型存入 model 中,在需要时取用。此外,table view 可能会分页,所以 model 最好能有一个 nextPage 的属性。
有了上面的讨论,我们给 model 下一个定义:
protocol Pageable {
var nextPage: Int? { get }
}
extension Pageable {
var nextPage: Int? { nil }
}
protocol ModelType: Pageable {
static var tCells: [UITableViewCell.Type]? { get }
static var tNibs: [UITableViewCell.Type]? { get }
// All cell types, sort by display order
static var tAll: [UITableViewCell.Type]? { get }
// Store model data in display order
var data: [[Any]]? { get }
}
extension ModelType {
static var tCells: [UITableViewCell.Type]? { nil }
static var tNibs: [UITableViewCell.Type]? { nil }
static var tAll: [UITableViewCell.Type]? { nil }
var data: [[Any]]? { nil }
}
使用
于是,我们以后使用 UITableView 可以这么做:
首先,让 model 遵循 ModelType:
extension SomeModel: Model {
static var tCells: [UITableViewCell.Type]? {
[ACell.self, BCell.self]
}
static var tNibs: [UITableViewCell.Type]? {
[CCell.self, DCell.self]
}
static var tAll: [UITableViewCell.Type]? {
// Sort by display order
[ACell.self, BCell.self, CCell.self, DCell.self]
}
var data: [[Any]]? {
[someA, someB, someC, someD]
}
}
接着,在 cell 中实现 configure(_:)
方法:
class SomeCell: UITableViewCell {
...
// Configure cell
override func configure(_ model: Any?) {
...
}
}
最后,在 ViewController 中:
1. 创建 model 对象
let someModel = SomeModel(..)
2. 注册 cell
override func viewDidLoad() {
super.viewDidLoad()
...
tableView.registerCells(with: SomeModel.tCells!)
tableView.registerNibs(with: SomeModel.tNibs!)
}
3. 创建并配置 cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(for: indexPath, with: SomeModel.tAll!)
cell.configure(someModel.data![indexPath.section][indexPath.row])
return cell
}
这里对 dequeueReusableCell(for:with:)
方法做了一些修改:
func dequeueReusableCell(
for indexPath: IndexPath,
with cells: [UITableViewCell.Type]
) -> some UITableViewCell {
for (index, cell) in cells.enumerated() where index == indexPath.section {
let cell = dequeueReusableCell(withIdentifier: cell.reuseIdentifier, for: indexPath)
return cell
}
fatalError()
}
UICollectionView 的解决方案与之类似,就不做介绍了。
下篇预告
为了实现刷新操作的统一处理,需要用到状态机,在网络上简单搜寻一番,没有发现让我满意的,于是,我自己写了一个。下篇文章会介绍这个状态机。
源码地址: YLExtensions