如何更好的实现 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。这一步大家根据自己的实际需求是写就可以了。

相关推荐
2401_865854889 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
HackerTom20 小时前
iOS用rime且导入自制输入方案
ios·iphone·rime
良技漫谈20 小时前
Rust移动开发:Rust在iOS端集成使用介绍
后端·程序人生·ios·rust·objective-c·swift
2401_8524035521 小时前
高效管理iPhone存储:苹果手机怎么删除相似照片
ios·智能手机·iphone
星际码仔1 天前
【动画图解】是怎样的方法,能被称作是 Flutter Widget 系统的核心?
android·flutter·ios
emperinter1 天前
WordCloudStudio:AI生成模版为您的文字云创意赋能 !
图像处理·人工智能·macos·ios·信息可视化·iphone
关键帧Keyframe1 天前
音视频面试题集锦第 8 期
ios·音视频开发·客户端
pb82 天前
引入最新fluwx2.5.4的时候报错
flutter·ios
袁代码2 天前
Swift 开发教程系列 - 第4章:函数与闭包
ios·swift·ios开发
#摩斯先生3 天前
IOS 防截屏实现
ios·xcode