如何更好的实现 UITableView 的空数据视图

前言

我们即使在用户体验方面没有专业知识也能意识到:用户是不喜欢模棱两可。用户需要一个即时和清晰的视觉指示器,显示他们正在使用的产品的当前状态。在 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。这一步大家根据自己的实际需求是写就可以了。

相关推荐
比格丽巴格丽抱4 小时前
flutter项目苹果编译运行打包上线
flutter·ios
网络安全-老纪5 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
1024小神8 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
lzhdim9 小时前
iPhone 17 Air看点汇总:薄至6mm 刷新苹果轻薄纪录
ios·iphone
安和昂9 小时前
【iOS】知乎日报第四周总结
ios
麦田里的守望者江12 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
_黎明13 小时前
【Swift】字符串和字符
开发语言·ios·swift
ZVAyIVqt0UFji15 小时前
iOS屏幕共享技术实践
macos·ios·objective-c·cocoa
hfxns_16 小时前
iOS 18.2 Beta 4开发者预览版发布,相机新增辅助功能
ios
AirDroid_cn1 天前
如何控制自己玩手机的时间?两台苹果手机帮助自律
ios·智能手机·ipad·手机使用技巧·苹果手机使用技巧