前语
咱们即使在用户体验方面没有专业知识也能意识到:用户是不喜欢不置可否。用户需求一个即时和明晰的视觉指示器,显现他们正在运用的产品的当前状态。在 app 中,假如咱们的列表页面没有数据的话,咱们需求显现一些自定义内容来告诉用户。
UITableView
的正常运用通常都是十分简略的。你会创立一个它的实例目标,然后将对应的控制器设置为它的 .delegate
/.dataSource
。然后运用 numberOfRowsInSection
/ cellForRowAt
等代理方法去完成相应的逻辑:
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
。
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
来指定如何显现空表视图数据:
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元素增加布局束缚。表格视图单元格只创立并具有子视图,因此表格视图单元格能够在多个布局中复用。
现在功用已完成,咱们能够增加两个扩展方法来显现或躲藏空单元格:
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。这一步我们依据自己的实践需求是写就能够了。