147

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 screen, such as:

This list is now empty

What is the simplest way to do it?

shim
  • 9,289
  • 12
  • 69
  • 108
cateof
  • 6,608
  • 25
  • 79
  • 153

24 Answers24

200

UITableView's backgroundView property is your friend.

In viewDidLoad or anywhere that you reloadData you should determine if there your table is empty or not and update the UITableView's backgroundView property with a UIView containing a UILabel or just set it to nil. That's it.

It is of course possible to make UITableView's data source do double duty and return a special "list is empty" cell, it strikes me as a kludge. Suddenly numberOfRowsInSection:(NSInteger)section has to compute the number of rows of other sections it wasn't asked about to make sure they are empty too. You also need to make a special cell that has the empty message. Also don't forget that you need to probably change the height of your cell to accommodate the empty message. This is all doable but it seems like band-aid on top of band-aid.

Ray Fix
  • 5,575
  • 3
  • 28
  • 30
  • 14
    The backgroundView is the best solution, I think. Thank you! – Ferran Maylinch May 07 '14 at 13:46
  • 1
    One disadvantage is that if the table view is pulled down the message stays on its position. I'd rather like to have like in the app store app under updates. Here the empty message goes with the scrolling behavior ... – testing Nov 06 '14 at 13:28
  • @testing obviously if you need the empty messaging to scroll you can't use background view as that is not part of the scroll hierarchy. You probably want to use a custom section and UITableViewCell for the empty state. – LightningStryk Sep 04 '15 at 16:29
  • Toggle `backgroundView` hidden property is the way to go. With `DZNEmptyDataSet`, we have to use its `emptyDataSetSource ` – onmyway133 Oct 20 '15 at 05:05
  • If you want to add buttons, you must do: `tableView.backgroundView!.userInteraction = true` after the line you set the `tableView.backgroundView = constructMyViewWithButtons()`, or however you set it. – kbpontius Apr 01 '16 at 02:40
  • did somebody make it working to use a button on backgroundView in UI tests? Won't be find here. – Tobe Jul 11 '18 at 12:04
  • Its crashing for me when trying to set tableFooterView = UIView(frame: CGRect.zero) – mohsin Mar 15 '19 at 06:49
130

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
}
Renan Lopes
  • 1,229
  • 2
  • 10
  • 17
Frankie
  • 11,508
  • 5
  • 53
  • 60
91

Based on the answers here, here is a quick class I made that you can use on in your UITableViewController.

import Foundation
import UIKit

class TableViewHelper {

    class func EmptyMessage(message:String, viewController:UITableViewController) {
        let rect = CGRect(origin: CGPoint(x: 0,y :0), size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height))
        let messageLabel = UILabel(frame: rect)
        messageLabel.text = message
        messageLabel.textColor = UIColor.blackColor()
        messageLabel.numberOfLines = 0;
        messageLabel.textAlignment = .Center;
        messageLabel.font = UIFont(name: "TrebuchetMS", size: 15)
        messageLabel.sizeToFit()

        viewController.tableView.backgroundView = messageLabel;
        viewController.tableView.separatorStyle = .None;
    }
}

In your UITableViewController you can call this in numberOfSectionsInTableView(tableView: UITableView) -> Int

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    if projects.count > 0 {
        return 1
    } else {
        TableViewHelper.EmptyMessage("You don't have any projects yet.\nYou can create up to 10.", viewController: self)
        return 0
    }
}

enter image description here

With a little help from http://www.appcoda.com/pull-to-refresh-uitableview-empty/

Jens
  • 6,243
  • 1
  • 49
  • 79
Johnston
  • 20,196
  • 18
  • 72
  • 121
  • would also like to add image to it, though. But a great quick solution. – Anjan Biswas Jun 26 '17 at 18:10
  • 11
    use an extension instead of a class (don't pass the view controller) – Cesare Mar 21 '18 at 21:11
  • I was not able to get the code above to work for me, but I was able to get the code in this answer (first one) to work, in case others have the same problem. Also I think the 2nd answer would work as well (using the scene dock) - https://stackoverflow.com/questions/28532926/if-no-table-view-results-display-no-results-on-screen/32708294 – Renee Olson May 23 '18 at 22:23
  • `viewController.tableView.separatorStyle = .none` isn't required. – Dmitry Dec 17 '19 at 02:25
18

I recommend the following library: DZNEmptyDataSet

The easiest way to add it in your project is to use it with Cocaopods like so: pod 'DZNEmptyDataSet'

In your TableViewController add the following import statement (Swift):

import DZNEmptyDataSet

Then make sure your class conforms to the DNZEmptyDataSetSource and DZNEmptyDataSetDelegate like so:

class MyTableViewController: UITableViewController, DZNEmptyDataSetSource, DZNEmptyDataSetDelegate

In your viewDidLoad add the following lines of code:

tableView.emptyDataSetSource = self
tableView.emptyDataSetDelegate = self
tableView.tableFooterView = UIView()

Now all you have to do to show the emptystate is:

//Add title for empty dataset
func titleForEmptyDataSet(scrollView: UIScrollView!) -> NSAttributedString! {
    let str = "Welcome"
    let attrs = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)]
    return NSAttributedString(string: str, attributes: attrs)
}

//Add description/subtitle on empty dataset
func descriptionForEmptyDataSet(scrollView: UIScrollView!) -> NSAttributedString! {
    let str = "Tap the button below to add your first grokkleglob."
    let attrs = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleBody)]
    return NSAttributedString(string: str, attributes: attrs)
}

//Add your image
func imageForEmptyDataSet(scrollView: UIScrollView!) -> UIImage! {
    return UIImage(named: "MYIMAGE")
}

//Add your button 
func buttonTitleForEmptyDataSet(scrollView: UIScrollView!, forState state: UIControlState) -> NSAttributedString! {
    let str = "Add Grokkleglob"
    let attrs = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleCallout)]
    return NSAttributedString(string: str, attributes: attrs)
}

//Add action for button
func emptyDataSetDidTapButton(scrollView: UIScrollView!) {
    let ac = UIAlertController(title: "Button tapped!", message: nil, preferredStyle: .Alert)
    ac.addAction(UIAlertAction(title: "Hurray", style: .Default, handler: nil))
    presentViewController(ac, animated: true, completion: nil)
}

These methods aren't mandatory, it's also possible to just show the empty state without a button etc.

For Swift 4

// MARK: - Deal with the empty data set
// Add title for empty dataset
func title(forEmptyDataSet _: UIScrollView!) -> NSAttributedString! {
    let str = "Welcome"
    let attrs = [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)]
    return NSAttributedString(string: str, attributes: attrs)
}

// Add description/subtitle on empty dataset
func description(forEmptyDataSet _: UIScrollView!) -> NSAttributedString! {
    let str = "Tap the button below to add your first grokkleglob."
    let attrs = [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)]
    return NSAttributedString(string: str, attributes: attrs)
}

// Add your image
func image(forEmptyDataSet _: UIScrollView!) -> UIImage! {
    return UIImage(named: "MYIMAGE")
}

// Add your button
func buttonTitle(forEmptyDataSet _: UIScrollView!, for _: UIControlState) -> NSAttributedString! {
    let str = "Add Grokkleglob"
    let attrs = [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: UIFontTextStyle.callout), NSAttributedStringKey.foregroundColor: UIColor.white]
    return NSAttributedString(string: str, attributes: attrs)
}

// Add action for button
func emptyDataSetDidTapButton(_: UIScrollView!) {
    let ac = UIAlertController(title: "Button tapped!", message: nil, preferredStyle: .alert)
    ac.addAction(UIAlertAction(title: "Hurray", style: .default, handler: nil))
    present(ac, animated: true, completion: nil)
}
Sunil Targe
  • 7,251
  • 5
  • 49
  • 80
Hapeki
  • 2,153
  • 1
  • 20
  • 36
11

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.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • 5
    I've had problems doing this with tables that have rows deleted with `deleteRowsAtIndexPaths:withRowAnimation:` as the number returned from `numberOfRowsInSection` needs to match the result of $numRows - $rowsDeleted. – Animal451 Jan 28 '14 at 08:48
  • 4
    I highly, highly recommend against this. It riddles your code with edge cases. – xtravar Mar 16 '14 at 23:29
  • 2
    @xtravar Rather than downvoting, consider posting your own answer. – Sergey Kalinichenko Mar 16 '14 at 23:44
  • Sorry, I have to maintain a large enterprise app and your answer has caused a million +1 headaches. I've posted my answer. – xtravar Mar 16 '14 at 23:54
  • @xtravar If you have an enterprise application with lots of `UITableView`s, you might as well go for some reusability, and build a table view data source to handle the empty case, and set it on the table view as soon as you learn that it's going to be empty. This way the code in the regular data source will stay clean. – Sergey Kalinichenko Mar 17 '14 at 00:17
  • 1
    I've been down that path, and it leads to its own array of problems. In my experience, it's almost always better to augment Apple APIs than try to replace them. – xtravar Mar 17 '14 at 04:00
  • 1
    This solution won't work with NSFetchedResultsController. I'm going to try the backgroundView approach instead. – Sam Jul 06 '14 at 07:06
11

I have been using the titleForFooterInSection message for this. I don't know if this is suboptimal or not, but it works.

-(NSString*)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section   {

    NSString *message = @"";
    NSInteger numberOfRowsInSection = [self tableView:self.tableView numberOfRowsInSection:section ];

    if (numberOfRowsInSection == 0) {
        message = @"This list is now empty";
    }

    return message;
}
Dominic
  • 3,304
  • 19
  • 22
Carlos B
  • 456
  • 3
  • 11
  • 2
    Nice trick and seems pretty clean to me. Here's a one-liner: `return tableView.numberOfRowsInSection(section) == 0 ? "This list is now empty" : nil` – TruMan1 Jul 24 '15 at 15:14
9

I can only recommend to drag&drop a UITextView inside the TableView after the cells. Make a connection to the ViewController and hide/display it when appropriate (e.g. whenever the table reloads).

enter image description here

pasql
  • 3,815
  • 5
  • 23
  • 33
8

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
     }
}
Meseery
  • 1,845
  • 2
  • 22
  • 19
5

Using the backgroundView is fine, but it does not scroll nicely like in Mail.app.

I did something similar to what xtravar did.

I added a view outside the view hierarchy of the tableViewController. hierarchy

Then i used the following code in tableView:numberOfRowsInSection::

if someArray.count == 0 {
    // Show Empty State View
    self.tableView.addSubview(self.emptyStateView)
    self.emptyStateView.center = self.view.center
    self.emptyStateView.center.y -= 60 // rough calculation here
    self.tableView.separatorColor = UIColor.clear
} else if self.emptyStateView.superview != nil {
    // Empty State View is currently visible, but shouldn't
    self.emptyStateView.removeFromSuperview()
    self.tableView.separatorColor = nil
}

return someArray.count

Basically I added the emptyStateView as a subview of the tableView object. As the separators would overlap the view, I set their color to clearColor. To get back to the default separator color, you can just set it to nil.

Community
  • 1
  • 1
mangerlahn
  • 4,746
  • 2
  • 26
  • 50
  • This works pretty well! But if you have elements for pull to refresh e.g., this doesn't scroll with them. I found it better to set the `tableHeaderView`, and make sure [it resizes to fit](http://stackoverflow.com/questions/5581116/how-to-set-the-height-of-table-header-in-uitableview). – Hannele Mar 07 '17 at 20:30
5

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:

enter image description here

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()
    }
    ...
}
Repose
  • 2,157
  • 1
  • 24
  • 26
4

Using a Container View Controller is the right way to do it according to Apple.

I put all my empty state views in a separate Storyboard. Each under it's own UIViewController subclass. I add content directly under their root view. If any action/button is needed, you now already have a controller to handle it.
Then its just a matter of instantiating the desired view controller from that Storyboard, add it as a child view controller and add the container view to the tableView's hierarchy (sub view). Your empty state view will be scrollable as well, which feels good and allow you to implement pull to refresh.

Read chapter 'Adding a Child View Controller to Your Content' for help on how to implement.

Just make sure you set the child view frame as (0, 0, tableView.frame.width, tableView.frame.height) and things will be centered and aligned properly.

AmitP
  • 5,353
  • 4
  • 35
  • 27
4

This is the best and simple solution.

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 60)];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 60)];
label.text = @"This list is empty";
label.center = self.view.center;
label.textAlignment = NSTextAlignmentCenter;
[view addSubview:label];

self.tableView.backgroundView = view;
pkamb
  • 33,281
  • 23
  • 160
  • 191
Rajesh Maurya
  • 3,026
  • 4
  • 19
  • 37
3

First, the problems with other popular approaches.

BackgroundView

Background view doesn't center nicely if you were to use the simple case of setting it to be a UILabel.

Cells, headers, or footers to display the message

This interferes with your functional code and introduces weird edge cases. If you want to perfectly center your message, that adds another level of complexity.

Rolling your own table view controller

You lose built-in functionality, such as refreshControl, and re-invent the wheel. Stick to UITableViewController for the best maintainable results.

Adding UITableViewController as a child view controller

I have a feeling you'll end up with contentInset issues in iOS 7+ - plus why complicate things?

My solution

The best solution I've come up with (and, granted, this isn't ideal) is to make a special view that can sit on top of a scroll view and act accordingly. This obviously gets complicated in iOS 7 with contentInset madness, but it's doable.

Things you have to watch out for:

  • table separators get brought to front at some point during reloadData - you need to guard against that
  • contentInset/contentOffset - observe those keys on your special view
  • keyboard - if you don't want the keyboard to get in the way, that's another calculation
  • autolayout - you can't depend on frame changes to position your view

Once you have this figured out once in one UIView subclass, you can use it for everything - loading spinners, disabling views, showing error messages, etc.

xtravar
  • 1,321
  • 11
  • 24
  • Can you elaborate in your answer. How do you tell if the table is empty? To then "act accordingly". – Michael Ozeryansky Nov 02 '14 at 23:06
  • You know your table is empty because you are the one populating your table. So in your view controller, you'd have an asynchronous loading callback. In that callback, if(itemsToShow.count == 0) { add your view to the scroll view }. The tricky part is making that top view ('shield/message view') positioned to take up the entire frame of the scroll view, minus contentInset, etc., so that the message is centered vertically. – xtravar Nov 03 '14 at 20:43
  • How do you add a view on top of the table view? self.view is the table view and if I use `addSubView` it's attached to the table view which always makes problems with auto layout. – testing Nov 06 '14 at 13:31
  • The view you place within the table view should not use autolayout, and the tableview itself does not use autolayout. – xtravar Nov 06 '14 at 21:23
  • 1
    You missed one approach there; You could use a UITableViewController and add it as a child view controller to a regular UIViewController – user1169629 Aug 20 '15 at 09:03
  • I did mention that "as a child view controller". I would anticipate inset issues with UINavigationController, and you'd have to do this rote code every time you do a table view. – xtravar Oct 30 '15 at 17:19
3

You can add this to your Base class.

var messageLabel = UILabel()

func showNoDataMessage(msg: String) {
    let rect = CGRect(origin: CGPoint(x: 0, y :self.view.center.y), size: CGSize(width: self.view.bounds.width - 16, height: 50.0))
    messageLabel = UILabel(frame: rect)
    messageLabel.center = self.view.center
    messageLabel.text = msg
    messageLabel.numberOfLines = 0
    messageLabel.textColor = Colors.grayText
    messageLabel.textAlignment = .center;
    messageLabel.font = UIFont(name: "Lato-Regular", size: 17)
    self.view.addSubview(messageLabel)
    self.view.bringSubviewToFront(messageLabel)
}

Show it like this in the class on getting the data from api.

func populateData(dataSource : [PRNJobDataSource]){
    self.dataSource = dataSource
    self.tblView.reloadData()
    if self.dataSource.count == 0 {self.showNoDataMessage(msg: "No data found.")}
}

Hide it like this.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if self.dataSource.count > 0 {self.hideNoDataMessage()}
    return dataSource.count
}

func hideNoDataMessage(){
    messageLabel.removeFromSuperview()
}
Mudassir Asghar
  • 218
  • 2
  • 11
2

Select your tableviewController Scene in storyboard

enter image description here

Drag and drop UIView Add label with your message (eg: No Data)

enter image description here

create outlet of UIView (say for eg yournoDataView) on your TableViewController.

and in viewDidLoad

self.tableView.backgroundView = yourNoDataView

2

Show Message for empty list, Wether its UITableView or UICollectionView.

extension UIScrollView {
    func showEmptyListMessage(_ message:String) {
        let rect = CGRect(origin: CGPoint(x: 0,y :0), size: CGSize(width: self.bounds.size.width, height: self.bounds.size.height))
        let messageLabel = UILabel(frame: rect)
        messageLabel.text = message
        messageLabel.textColor = .black
        messageLabel.numberOfLines = 0
        messageLabel.textAlignment = .center
        messageLabel.font = UIFont.systemFont(ofSize: 15)
        messageLabel.sizeToFit()

        if let `self` = self as? UITableView {
            self.backgroundView = messageLabel
            self.separatorStyle = .none
        } else if let `self` = self as? UICollectionView {
            self.backgroundView = messageLabel
        }
    }
}

Usages:

if cellsViewModels.count == 0 {
    self.tableView.showEmptyListMessage("No Product In List!")
}

OR:

if cellsViewModels.count == 0 {
    self.collectionView?.showEmptyListMessage("No Product In List!")
}

Remember: Don't forget to remove the message label in case data will come after refresh.

Gurjit Singh
  • 1,723
  • 21
  • 27
1

The easiest and quickest way to do this is to drag a label on to side panel under tableView. Create a outlet for the label and the tableView and add a if statement to hide and show the label and table as needed. Alternatively you can add tableView.tableFooterView = UIView(frame: CGRect.zero) this to you viewDidLoad() to give an empty table the perception that it is hidden if the table and background view have the same colour.

yoamod
  • 31
  • 4
  • (This post does not seem to provide a [quality answer](https://stackoverflow.com/help/how-to-answer) to the question. Please either edit your answer and complete it with more detailed information, or just post it as a comment to the question). – sɐunıɔןɐqɐp Aug 16 '18 at 11:52
1

Using Swift 4.2

  func numberOfSections(in tableView: UITableView) -> Int
{
    var numOfSections: Int = 0
    if self.medArray.count > 0
    {
        tableView.separatorStyle = .singleLine
        numOfSections            = 1
        tableView.backgroundView = nil
    }
    else
    {
        let noDataLabel: UILabel  = UILabel(frame: CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: tableView.bounds.size.height))
        noDataLabel.text          = "No  Medicine available.Press + to add New Pills "
        noDataLabel.textColor     = UIColor.black
        noDataLabel.textAlignment = .center
        tableView.backgroundView  = noDataLabel
        tableView.separatorStyle  = .none
    }
    return numOfSections
}
M Murteza
  • 1,629
  • 15
  • 10
1

I made a few changes so that we don't need to check on the count manually, also i added constraints for the label so that nothing goes wrong no matter how large is the message as shown below:

extension UITableView {

    fileprivate func configureLabelLayout(_ messageLabel: UILabel) {
        messageLabel.translatesAutoresizingMaskIntoConstraints = false
        let labelTop: CGFloat = CGFloat(UIDevice.current.userInterfaceIdiom == .pad ? 25:15)
        messageLabel.topAnchor.constraint(equalTo: backgroundView?.topAnchor ?? NSLayoutAnchor(), constant: labelTop).isActive = true
        messageLabel.widthAnchor.constraint(equalTo: backgroundView?.widthAnchor ?? NSLayoutAnchor(), constant: -20).isActive = true
        messageLabel.centerXAnchor.constraint(equalTo: backgroundView?.centerXAnchor ?? NSLayoutAnchor(), constant: 0).isActive = true
    }

    fileprivate func configureLabel(_ message: String) {
        let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
        messageLabel.textColor = .black
        messageLabel.numberOfLines = 0
        messageLabel.textAlignment = .center
        let fontSize = CGFloat(UIDevice.current.userInterfaceIdiom == .pad ? 25:15)
        let font: UIFont = UIFont(name: "MyriadPro-Regular", size: fontSize) ?? UIFont()
        messageLabel.font = font
        messageLabel.text = message
        self.backgroundView = UIView()
        self.backgroundView?.addSubview(messageLabel)
        configureLabelLayout(messageLabel)
        self.separatorStyle = .none
    }

    func setEmptyMessage(_ message: String, _ isEmpty: Bool) {
        if isEmpty { // instead of making the check in every TableView DataSource in the project
            configureLabel(message)
        }
        else {
            restore()
        }

    }

    func restore() {
        self.backgroundView = nil
        self.separatorStyle = .singleLine
    }
}

Usage

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let message: String = "The list is empty."
        ticketsTableView.setEmptyMessage(message, tickets.isEmpty)
        return self.tickets.count
    }
Mostafa Sultan
  • 2,268
  • 2
  • 20
  • 36
0

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;
        }
Ravindra Shekhawat
  • 4,275
  • 1
  • 19
  • 26
0

Probably not the greatest solution, but I did this by just putting a label at the bottom of my table and if the rows = 0 then I assign it some text. Pretty easy, and achieves what you are trying to do with a few lines of code.

I have two sections in my table (jobs and schools)

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    if (jobs.count == 0 && schools.count == 0) {
        emptyLbl.text = "No jobs or schools"
    } else {
        emptyLbl.text = ""
    }
Zach
  • 374
  • 3
  • 8
  • You could also replace the label with an empty imageview when count = 0 if you wanted.. – Zach Jul 05 '17 at 07:39
0

Or alternatively you can use a bit customizable light-weight library

SwiftEmptyState

Enes Karaosman
  • 1,889
  • 20
  • 27
0

In my case, I also wanted to add an image on top of the text like in the image below: background view with text and image

So I followed the following steps:

  1. I created a custom UIView to set as my UITableView backgroundView.. I followed the steps in this blog post: https://betterprogramming.pub/swift-3-creating-a-custom-view-from-a-xib-ecdfe5b3a960
  2. I then created an extension of UITableView and added methods to manipulate states as follows:
extension UITableView {
    func showEmptyView(image: UIImage? = nil, message: String? = nil) {
        self.backgroundView = nil
        
        let emptyListBackgroundView = UITableViewBackgroundView.init(frame: self.bounds)
       
        if let image = image {
            emptyListBackgroundView.infoImageView.image = image
        }
        
        if let message = message {
            emptyListBackgroundView.infoLabel.text = message
        }
        
        self.backgroundView = emptyListBackgroundView
        
    }
    
    func restore() {
        self.backgroundView = nil
    }
}

I then used it in my ViewController like this:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if data.count == 0 {
        tableView.showEmptyView()
    } else {
        tableView.restore()
    }  

    return data.count   
}
angtlin
  • 192
  • 1
  • 9
NhlanhlaNkosi
  • 573
  • 1
  • 4
  • 8
0

I just updated @Frankie answer ... and put an image on top of message view...

   func setEmptyMessage(_ message: String, imageName: String) {
        
        let emptyStateView = UIView(frame: CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height))
        
        // Create an image view
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(named: imageName)
        
        // Calculate the scaled size for the image
        let maxWidth = emptyStateView.bounds.width * 0.4 // Adjust the scaling as needed
        let imageSize = scaleImageSize(imageView.image?.size ?? CGSize.zero, maxWidth: maxWidth)
        
        
        // Create a label
        let messageLabel = UILabel()
        messageLabel.text = message
        messageLabel.textColor = .gray
        messageLabel.numberOfLines = 0
        messageLabel.textAlignment = .center
//        messageLabel.font = UIFont(name: "TrebuchetMS", size: 15)
        messageLabel.font = UIFont(name: Constants.AppFonts.mediumEn, size: 20)
        messageLabel.sizeToFit()
        
        // Set the frame for image view and label
        let totalHeight = imageSize.height + messageLabel.frame.height + 16
        let verticalSpacing: CGFloat = 8.0
        
        let imageY = (emptyStateView.frame.height - totalHeight) / 2
        let labelY = imageY + imageSize.height + verticalSpacing
        
        imageView.frame = CGRect(x: (emptyStateView.frame.width - imageSize.width) / 2,
                                 y: imageY, width: imageSize.width, height: imageSize.height)
        messageLabel.frame = CGRect(x: 0, y: labelY,
                                    width: emptyStateView.frame.width,
                                    height: messageLabel.frame.height)
        
        // Add image view and label to the empty state view
        emptyStateView.addSubview(imageView)
        emptyStateView.addSubview(messageLabel)
        
        // Set the empty state view as the background view
        self.backgroundView = emptyStateView
        self.separatorStyle = .none
        
    }
    
    func restore() {
        self.backgroundView = nil
        self.separatorStyle = .singleLine
    }
Wahab Khan Jadon
  • 875
  • 13
  • 21