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
Same as Jhonston's answer, but I preferred it as an extension:
import UIKit
extension UITableView {
func setEmptyMessage(_ message: String) {
let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
messageLabel.text = message
messageLabel.textColor = .black
messageLabel.numberOfLines = 0
messageLabel.textAlignment = .center
messageLabel.font = UIFont(name: "TrebuchetMS", size: 15)
messageLabel.sizeToFit()
self.backgroundView = messageLabel
self.separatorStyle = .none
}
func restore() {
self.backgroundView = nil
self.separatorStyle = .singleLine
}
}
Usage:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if things.count == 0 {
self.tableView.setEmptyMessage("My Message")
} else {
self.tableView.restore()
}
return things.count
}
One way of doing it would be modifying your data source to return 1
when the number of rows is zero, and to produce a special-purpose cell (perhaps with a different cell identifier) in the tableView:cellForRowAtIndexPath:
method.
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger actualNumberOfRows = <calculate the actual number of rows>;
return (actualNumberOfRows == 0) ? 1 : actualNumberOfRows;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger actualNumberOfRows = <calculate the actual number of rows>;
if (actualNumberOfRows == 0) {
// Produce a special cell with the "list is now empty" message
}
// Produce the correct cell the usual way
...
}
This may get somewhat complicated if you have multiple table view controllers that you need to maintain, because someone will eventually forget to insert a zero check. A better approach is to create a separate implementation of a UITableViewDataSource
implementation that always returns a single row with a configurable message (let's call it EmptyTableViewDataSource
). When the data that is managed by your table view controller changes, the code that manages the change would check if the data is empty. If it is not empty, set your table view controller with its regular data source; otherwise, set it with an instance of the EmptyTableViewDataSource
that has been configured with the appropriate message.
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()
}
...
}
Select your tableviewController Scene in storyboard
Drag and drop UIView Add label with your message (eg: No Data)
create outlet of UIView (say for eg yournoDataView) on your TableViewController.
and in viewDidLoad
self.tableView.backgroundView = yourNoDataView
Swift version but better and simpler form . **3.0
I hope it server your purpose......
In your UITableViewController .
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searchController.isActive && searchController.searchBar.text != "" {
if filteredContacts.count > 0 {
self.tableView.backgroundView = .none;
return filteredContacts.count
} else {
Helper.EmptyMessage(message: ConstantMap.NO_CONTACT_FOUND, viewController: self)
return 0
}
} else {
if contacts.count > 0 {
self.tableView.backgroundView = .none;
return contacts.count
} else {
Helper.EmptyMessage(message: ConstantMap.NO_CONTACT_FOUND, viewController: self)
return 0
}
}
}
Helper Class with function :
/* Description: This function generate alert dialog for empty message by passing message and
associated viewcontroller for that function
- Parameters:
- message: message that require for empty alert message
- viewController: selected viewcontroller at that time
*/
static func EmptyMessage(message:String, viewController:UITableViewController) {
let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: viewController.view.bounds.size.width, height: viewController.view.bounds.size.height))
messageLabel.text = message
let bubbleColor = UIColor(red: CGFloat(57)/255, green: CGFloat(81)/255, blue: CGFloat(104)/255, alpha :1)
messageLabel.textColor = bubbleColor
messageLabel.numberOfLines = 0;
messageLabel.textAlignment = .center;
messageLabel.font = UIFont(name: "TrebuchetMS", size: 18)
messageLabel.sizeToFit()
viewController.tableView.backgroundView = messageLabel;
viewController.tableView.separatorStyle = .none;
}
So for a safer solution:
extension UITableView {
func setEmptyMessage(_ message: String) {
guard self.numberOfRows() == 0 else {
return
}
let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
messageLabel.text = message
messageLabel.textColor = .black
messageLabel.numberOfLines = 0;
messageLabel.textAlignment = .center;
messageLabel.font = UIFont.systemFont(ofSize: 14.0, weight: UIFontWeightMedium)
messageLabel.sizeToFit()
self.backgroundView = messageLabel;
self.separatorStyle = .none;
}
func restore() {
self.backgroundView = nil
self.separatorStyle = .singleLine
}
public func numberOfRows() -> Int {
var section = 0
var rowCount = 0
while section < numberOfSections {
rowCount += numberOfRows(inSection: section)
section += 1
}
return rowCount
}
}
and for UICollectionView
as well:
extension UICollectionView {
func setEmptyMessage(_ message: String) {
guard self.numberOfItems() == 0 else {
return
}
let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
messageLabel.text = message
messageLabel.textColor = .black
messageLabel.numberOfLines = 0;
messageLabel.textAlignment = .center;
messageLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFontWeightSemibold)
messageLabel.sizeToFit()
self.backgroundView = messageLabel;
}
func restore() {
self.backgroundView = nil
}
public func numberOfItems() -> Int {
var section = 0
var itemsCount = 0
while section < self.numberOfSections {
itemsCount += numberOfItems(inSection: section)
section += 1
}
return itemsCount
}
}
More Generic Solution:
protocol EmptyMessageViewType {
mutating func setEmptyMessage(_ message: String)
mutating func restore()
}
protocol ListViewType: EmptyMessageViewType where Self: UIView {
var backgroundView: UIView? { get set }
}
extension UITableView: ListViewType {}
extension UICollectionView: ListViewType {}
extension ListViewType {
mutating func setEmptyMessage(_ message: String) {
let messageLabel = UILabel(frame: CGRect(x: 0,
y: 0,
width: self.bounds.size.width,
height: self.bounds.size.height))
messageLabel.text = message
messageLabel.textColor = .black
messageLabel.numberOfLines = 0
messageLabel.textAlignment = .center
messageLabel.font = UIFont(name: "TrebuchetMS", size: 16)
messageLabel.sizeToFit()
backgroundView = messageLabel
}
mutating func restore() {
backgroundView = nil
}
}