前言
我们即使在用户体验方面没有专业知识也能意识到:用户是不喜欢模棱两可。用户需要一个即时和清晰的视觉指示器,显示他们正在使用的产品的当前状态。在 app 中,如果我们的列表页面没有数据的话,我们需要显示一些自定义内容来告诉用户。
UITableView
的正常使用通常都是非常简单的。你会创建一个它的实例对象,然后将对应的控制器设置为它的 .delegate
/.dataSource
。然后使用 numberOfRowsInSection
/ cellForRowAt
等代理方法去实现相应的逻辑:
swift
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch data.count == 0 {
case true:
return 1
case false:
return data.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch data.count == 0 {
case true:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "empty-cell-id", for: indexPath) as? EmptyTableViewCell
else { return EmptyTableViewCell() }
let noResultsView = NoResultsView()
noResultsView.setContent()
cell.setup(view: noResultsView)
return cell
case false:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell-id", for: indexPath) as? DataTableViewCell
else { return DataTableViewCell() }
cell.setContent(data: data[indexPath.row])
return cell
}
}
比如上面的示例代码,我们在 numberOfRowsInSection
里执行了下面的逻辑:
- 如果数据是空的,我们就返回一条单元格
- 如果数据不为空,则返回数据源的所有单元格
cellForRowAt
里的逻辑与之类似:
- 如果数据是空的,返回我们自定义显示空状态的单元格
- 如果数据不为空,则正常显示我们需要的视图样式
上面这种做法是可以实现需求的,但这通常会使代码难以维护且不可重用。如果我们想在多个地方重用逻辑,我们将需要复制和粘贴相同的逻辑。想象一下,如果设计师重新设计了一个空的表格视图单元格,那么我们必须找到并改变所有地方的逻辑。
并且 switch - case
这种情况看起来不难看吗?我们必须始终记住在两个方法中保持两个 switch
语句逻辑同步。
优化方案
不同的 UITableViewDataSource
应该是独立的。毕竟,所有数据源所做的只是提供有关如何呈现表视图的说明。例如,如果我们想要显示一个用户列表,我们就为表视图提供一个假设的用户数据源。如果我们想要显示一个空的表视图单元格,我们可以为表视图提供一个不同的 .datasource
,然后重新加载以刷新单元格。如果后端稍后告诉我们用户列表不再为空,我们可以再次交换数据源。
我们需要一个专门的表视图子类来管理数据源。将它命名为 EmptyTableView
。
swift
class EmptyTableView: UITableView {
var emptyDataSource: EmptyTableViewDataSource
var emptyTableViewDelegate: EmptyTableViewDelegate?
var normalDataSource: UITableViewDataSource
var normalTableViewDelegate: UITableViewDelegate?
init(emptyDataSource: EmptyTableViewDataSource,
normalDataSource: UITableViewDataSource,
emptyTableViewDelegate: EmptyTableViewDelegate? = nil,
normalTableViewDelegate: UITableViewDelegate? = nil) {
self.emptyDataSource = emptyDataSource
self.normalDataSource = normalDataSource
self.emptyTableViewDelegate = emptyTableViewDelegate
self.normalTableViewDelegate = normalTableViewDelegate
super.init(frame: .zero, style: .plain)
dataSource = normalDataSource
delegate = normalTableViewDelegate
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
1、在构造期间注入空表视图的所有依赖属性。
2、它需要一个EmptyTableViewDataSource
, EmptyTableViewDelegate
,一个正常的UITableViewDataSource
和一个正常的 UITableViewDelegate
来工作。
3、如果不打算与表视图交互,则可以省略这两个表视图委托。
然后我们通过构造一个独立的 EmptyTableViewDataSource
来指定如何显示空表视图数据:
swift
class EmptyTableViewDataSource: NSObject, UITableViewDataSource {
let identifier: String
let emptyModel: EmptyTableViewCellModel
let cellConfigurator: EmptyTableViewCellConfigurator
init(identifier: String = "empty-table-view-cell",
emptyModel: EmptyTableViewCellModel = EmptyTableViewCellModel(),
cellConfigurator: EmptyTableViewCellConfigurator = PlainEmptyTableViewCellConfigurator()) {
self.identifier = identifier
self.emptyModel = emptyModel
self.cellConfigurator = cellConfigurator
super.init()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? EmptyTableViewCell else { return EmptyTableViewCell() }
cellConfigurator.configure(cell: cell, forDisplaying: emptyModel)
return cell
}
}
struct EmptyTableViewCellModel {
var icon: UIImage
var descriptionText: String
var secondaryText: String
var actionText: String
var action: (() -> ())?
init(icon: UIImage = UIImage(named: "default-empty-image")!,
descriptionText: String = NSLocalizedString("empty-cell-description", value: "It's kinda lonely out there", comment: "Empty table view cell description text"),
secondaryText: String = "",
actionText: String = "",
action: (() -> ())? = nil) {
self.icon = icon
self.descriptionText = descriptionText
self.secondaryText = secondaryText
self.actionText = actionText
self.action = action
}
}
protocol EmptyTableViewCellConfigurator {
func configure(cell: EmptyTableViewCell, forDisplaying emptyModel: EmptyTableViewCellModel)
}
class PlainEmptyTableViewCellConfigurator: EmptyTableViewCellConfigurator {
func configure(cell: EmptyTableViewCell, forDisplaying emptyModel: EmptyTableViewCellModel) {
guard let cell = cell as? PlainEmptyTableViewCell else { return }
cell.selectionStyle = .none
cell.icon = emptyModel.icon
cell.descriptionText = emptyModel.descriptionText
if emptyModel.actionText.isEmpty {
cell.actionButton.isHidden = true
} else {
cell.actionText = emptyModel.actionText
}
cell.iconImageView.image = emptyModel.icon
cell.descriptionLabel.text = emptyModel.descriptionText
cell.actionButton.setTitle(emptyModel.actionText, for: .normal)
cell.action = emptyModel.action
}
}
1、如果你在代码库上复用了统一的设计,那么将它们封装在一个结构体中以加强模型的结构通常是一个好主意。EmptyTableViewCellModel
用作空表视图单元格的数据。
2、EmptyTableViewCellConfigurator
是使表视图单元格可重用的又一步。例如,如果单元格具有相同的UI元素,例如图像、标签和按钮,但是布局不同,我们可以创建一个单元格配置器来处理向UI元素添加布局约束。表格视图单元格只创建并拥有子视图,因此表格视图单元格可以在多个布局中复用。
现在功能已完成,我们可以添加两个扩展方法来显示或隐藏空单元格:
swift
extension EmptyTableView {
func showEmptyCell() {
guard self.emptyDataSource !== self.dataSource else {
reloadData()
return
}
self.delegate = self.emptyTableViewDelegate
self.dataSource = self.emptyDataSource
self.reloadData()
}
func hideEmptyCell() {
guard self.normalDataSource !== self.dataSource else {
reloadData()
return
}
self.delegate = self.normalTableViewDelegate
self.dataSource = self.normalDataSource
self.reloadData()
}
}
剩下要做的就是构建空的表视图单元 UI。这一步大家根据自己的实际需求是写就可以了。