前语

咱们即使在用户体验方面没有专业知识也能意识到:用户是不喜欢不置可否。用户需求一个即时和明晰的视觉指示器,显现他们正在运用的产品的当前状态。在 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。这一步我们依据自己的实践需求是写就能够了。