December 2, 2015 · Swift

iOS Empty Table View with Swift

If your iOS app contains a table view it's nice to have a dedicated view for the empty state. When there are no items to display in the table view, you want to make this clear to the user and show some type of message and/or image. An empty view can make the function of a particular menu clear to the user. I also find it's nicer to show a customized message and image instead of empty table row cells. In this tutorial I'll demonstrate how I created an empty view for a table view. I used this example in my app Voyageur. I will cover the code used in this sample app. This is how it looks in Voyageur:

Setup a view controller

I'll create a new project in Xcode with Cocoapods. To create the empty view I use PureLayout, which creates auto layout constraints. In the storyboard I embed a view controller in a navigation controller. Here is the initial view controller with an empty table:

import UIKit  
import PureLayout

class ViewController: UIViewController {

    private var tableView: UITableView!

    private var didSetupConstraints = false

    // MARK: - View Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }

    // MARK: - Initialization

    func setupTableView() {
        tableView = UITableView.newAutoLayoutView()
        view.addSubview(tableView)
    }

    // MARK: - Layout

    override func updateViewConstraints() {
        if !didSetupConstraints {
            tableView.autoPinToTopLayoutGuideOfViewController(self, withInset: 0)
            tableView.autoPinToBottomLayoutGuideOfViewController(self, withInset: 0)
            tableView.autoPinEdgeToSuperviewEdge(.Leading)
            tableView.autoPinEdgeToSuperviewEdge(.Trailing)
            didSetupConstraints = true
        }

        super.updateViewConstraints()
    }
}

This will give you a table view with empty rows.

Of course, the goal of this tutorial is to create a view, where in this situation a message would be displayed instead of empty rows.

Create the empty view

The next step is to create the view that you want to display instead of empty rows. I do this with a subclass of UIView, which I will set as the backgroundView of the table view. I created a class called EmptyBackgroundView, which allows you to initialize it with an image, top message and bottom message. It uses auto layout constraints to adjust the positioning of the labels and image.

import UIKit  
import PureLayout

class EmptyBackgroundView: UIView {

    private var topSpace: UIView!
    private var bottomSpace: UIView!
    private var imageView: UIImageView!
    private var topLabel: UILabel!
    private var bottomLabel: UILabel!

    private let topColor = UIColor.darkGrayColor()
    private let topFont = UIFont.boldSystemFontOfSize(22)
    private let bottomColor = UIColor.grayColor()
    private let bottomFont = UIFont.systemFontOfSize(18)

    private let spacing: CGFloat = 10
    private let imageViewHeight: CGFloat = 100
    private let bottomLabelWidth: CGFloat = 300

    var didSetupConstraints = false

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupViews()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }

    init(image: UIImage, top: String, bottom: String) {
        super.init(frame: CGRectZero)
        setupViews()
        setupImageView(image)
        setupLabels(top, bottom: bottom)
    }

    func setupViews() {
        topSpace = UIView.newAutoLayoutView()
        bottomSpace = UIView.newAutoLayoutView()
        imageView = UIImageView.newAutoLayoutView()
        topLabel = UILabel.newAutoLayoutView()
        bottomLabel = UILabel.newAutoLayoutView()

        addSubview(topSpace)
        addSubview(bottomSpace)
        addSubview(imageView)
        addSubview(topLabel)
        addSubview(bottomLabel)
    }

    func setupImageView(image: UIImage) {
        imageView.image = image
        imageView.tintColor = topColor
    }

    func setupLabels(top: String, bottom: String) {
        topLabel.text = top
        topLabel.textColor = topColor
        topLabel.font = topFont

        bottomLabel.text = bottom
        bottomLabel.textColor = bottomColor
        bottomLabel.font = bottomFont
        bottomLabel.numberOfLines = 0
        bottomLabel.textAlignment = .Center
    }

    override func updateConstraints() {
        if !didSetupConstraints {
            topSpace.autoAlignAxisToSuperviewAxis(.Vertical)
            topSpace.autoPinEdgeToSuperviewEdge(.Top)
            bottomSpace.autoAlignAxisToSuperviewAxis(.Vertical)
            bottomSpace.autoPinEdgeToSuperviewEdge(.Bottom)
            topSpace.autoSetDimension(.Height, toSize: spacing, relation: .GreaterThanOrEqual)
            topSpace.autoMatchDimension(.Height, toDimension: .Height, ofView: bottomSpace)

            imageView.autoPinEdge(.Top, toEdge: .Bottom, ofView: topSpace)
            imageView.autoAlignAxisToSuperviewAxis(.Vertical)
            imageView.autoSetDimension(.Height, toSize: imageViewHeight, relation: .LessThanOrEqual)

            topLabel.autoAlignAxisToSuperviewAxis(.Vertical)
            topLabel.autoPinEdge(.Top, toEdge: .Bottom, ofView: imageView, withOffset: spacing)

            bottomLabel.autoAlignAxisToSuperviewAxis(.Vertical)
            bottomLabel.autoPinEdge(.Top, toEdge: .Bottom, ofView: topLabel, withOffset: spacing)
            bottomLabel.autoPinEdge(.Bottom, toEdge: .Top, ofView: bottomSpace)
            bottomLabel.autoSetDimension(.Width, toSize: bottomLabelWidth)

            didSetupConstraints = true
        }

        super.updateConstraints()
    }
}

In updateConstraints the labels and images are positioned. There's spacing of at least 10 px on the top and bottom of the view. The image is 100 px tall or less. The colors and fonts are also set up. There's an initializer that set the image, top label and bottom label.

Set the background view

Next, I'll set the an EmptyBackgroundView as the backgroundView of tableView. First to ViewController you need to add the following variables:

private let image = UIImage(named: "star-large")!.imageWithRenderingMode(.AlwaysTemplate)  
private let topMessage = "Favorites"  
private let bottomMessage = "You don't have any favorites yet. All your favorites will show up here."  

star-large is an image asset that I imported. Then I added the following function, setupEmptyBackgroundView to setup the empty view. Call this function in viewDidLoad after setupTableView.

func setupEmptyBackgroundView() {  
    let emptyBackgroundView = EmptyBackgroundView(image: image, top: topMessage, bottom: bottomMessage)
    tableView.backgroundView = emptyBackgroundView
}

One small fix is still needed to avoid errors with auto layout. At the end of setupTableView add:

view.setNeedsLayout()  
view.layoutIfNeeded()  

This forces the view to layout subviews before adding the background view. Now when you run this code the empty background view should show up. However, there's still one problem; the empty rows show up on top of the background view. If there is no data, you want the rows to be invisible. When there is data, you want the rows to be visible and the background view to be hidden.

Toggle the visibility of the empty view

To control visibility of the empty view and rows I'll add a data source for the table view. Add the following variables to the view controller:

private var rows = [String]()  
private let cellIdentifier = "Cell"  

To setupTableView add:

tableView.dataSource = self  
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)  

In an extension at the end of the view controller file add:

extension ViewController: UITableViewDataSource {  
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if rows.count == 0 {
            tableView.separatorStyle = .None
            tableView.backgroundView?.hidden = false
        } else {
            tableView.separatorStyle = .SingleLine
            tableView.backgroundView?.hidden = true
        }

        return rows.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
        cell.textLabel?.text = rows[indexPath.row]
        return cell
    }
}

numberOfRowsInSection is where the background view is hidden or shown. If there are no rows, it's shown and the separator style of the cells is none. If there are rows, it's hidden and there is a cell separator. Now you have a a working empty view. The final result looks like this:

Here is the code used in this tutorial.

Comments powered by Disqus