I have a UITableView that in some cases it is legal to be empty. So instead of showing the background image of the app, I would prefer to print a friendly message in the scr
There is a specific use case for multiple data sets and sections, where you need an empty state for each section.
You can use suggestions mentioned in multiple answers to this question - provide custom empty state cell.
I'll try to walk you through all the steps programmatically in more detail and hopefully, this will be helpful. Here's the result we can expect:
For simplicity's sake, we will work with 2 data sets (2 sections), those will be static.
I will also assume that you have the rest of your tableView logic working properly with datasets, tabvleView cells, and sections.
Swift 5, let's do it:
1. Create a custom empty state UITableViewCell class:
class EmptyTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Empty State Message"
label.font = .systemFont(ofSize: 16)
label.textColor = .gray
label.textAlignment = .left
label.numberOfLines = 1
return label
}()
private func setupView(){
contentView.addSubviews(label)
let layoutGuide = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
label.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
label.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor),
label.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
label.heightAnchor.constraint(equalToConstant: 50)
])
}
}
2. Add the following to your UITableViewController class to register your empty cell:
class TableViewController: UITableViewController {
...
let emptyCellReuseIdentifier = "emptyCellReuseIdentifier"
...
override func viewDidLoad(){
...
tableView.register(EmptyTableViewCell.self, forCellReuseIdentifier: emptyCellReuseIdentifier)
...
}
}
3. Now let's highlight some assumptions mentioned above:
class TableViewController: UITableViewController {
// 2 Data Sets
var firstDataSet: [String] = []
var secondDataSet: [String] = []
// Sections array
let sections: [SectionHeader] = [
.init(id: 0, title: "First Section"),
.init(id: 1, title: "Second Section")
]
...
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
sections.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section].title
}
...
}
struct SectionHeader {
let id: Int
let title: String
}
4. Now let's add some custom logic to our Data Source to handle Empty Rows in our sections. Here we are returning 1 row if a data set is empty:
class TableViewController: UITableViewController {
...
// MARK: - Table view data source
...
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section{
case 0:
let numberOfRows = firstDataSet.isEmpty ? 1 : firstDataSet.count
return numberOfRows
case 1:
let numberOfRows = secondDataSet.isEmpty ? 1 : secondDataSet.count
return numberOfRows
default:
return 0
}
}
...
}
5. Lastly, the most important "cellForRowAt indexPath":
class TableViewController: UITableViewController {
...
// MARK: - Table view data source
...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Handle Empty Rows State
switch indexPath.section {
case 0:
if firstDataSet.isEmpty {
if let cell = tableView.dequeueReusableCell(withIdentifier: emptyCellReuseIdentifier) as? EmptyTableViewCell {
cell.label.text = "First Data Set Is Empty"
return cell
}
}
case 1:
if secondDataSet.isEmpty {
if let cell = tableView.dequeueReusableCell(withIdentifier: emptyCellReuseIdentifier) as? EmptyTableViewCell {
cell.label.text = "Second Data Set Is Empty"
return cell
}
}
default:
break
}
// Handle Existing Data Sets
if let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as? TableViewCell {
switch indexPath.section {
case 0:
...
case 1:
...
default:
break
}
return cell
}
return UITableViewCell()
}
...
}