UITableView
iOS Development
Swift Programming
Mobile App UI
Empty State Design

Handling an empty UITableView. Print a friendly message

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

An empty UITableView should look intentional, not broken. The usual solution is to show a friendly empty-state view when the data source has no rows, then remove that view when content becomes available again.

Use backgroundView for the Empty State

The simplest pattern is to attach a label or custom view to the table view’s backgroundView. This keeps the empty state visually tied to the table and avoids extra layout complexity.

swift
1import UIKit
2
3extension UITableView {
4    func setEmptyMessage(_ message: String) {
5        let label = UILabel()
6        label.text = message
7        label.textColor = .secondaryLabel
8        label.textAlignment = .center
9        label.numberOfLines = 0
10        label.font = .preferredFont(forTextStyle: .body)
11
12        backgroundView = label
13        separatorStyle = .none
14    }
15
16    func restore() {
17        backgroundView = nil
18        separatorStyle = .singleLine
19    }
20}

This works well when a single sentence is enough to explain the state.

Update the Empty State When Data Changes

The empty-state logic should run whenever the data changes, not only in viewDidLoad.

swift
1import UIKit
2
3final class MessagesViewController: UIViewController, UITableViewDataSource {
4    @IBOutlet private weak var tableView: UITableView!
5    private var messages: [String] = []
6
7    override func viewDidLoad() {
8        super.viewDidLoad()
9        tableView.dataSource = self
10        refreshUI()
11    }
12
13    private func refreshUI() {
14        if messages.isEmpty {
15            tableView.setEmptyMessage("No messages yet.")
16        } else {
17            tableView.restore()
18        }
19
20        tableView.reloadData()
21    }
22
23    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
24        messages.count
25    }
26
27    func tableView(
28        _ tableView: UITableView,
29        cellForRowAt indexPath: IndexPath
30    ) -> UITableViewCell {
31        let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath)
32        cell.textLabel?.text = messages[indexPath.row]
33        return cell
34    }
35}

Keeping the empty-state decision in one helper method prevents drift between the visual state and the actual data.

Distinguish Empty, Filtered, and Error States

Zero rows do not always mean the same thing. A screen might be empty because:

  • the user has no content yet
  • a filter removed all matching rows
  • loading failed

Those states should not all use the same message. “No messages yet” is very different from “No results match your filter” or “Could not load messages. Pull to refresh.”

You can reuse the same backgroundView mechanism while changing the content according to the reason.

Use a Custom View for Richer Empty States

If a plain label is too limited, use a small stack view or a custom UIView instead.

swift
1let emptyView = UIStackView()
2emptyView.axis = .vertical
3emptyView.alignment = .center
4emptyView.spacing = 8
5
6let titleLabel = UILabel()
7titleLabel.text = "No items"
8titleLabel.font = .preferredFont(forTextStyle: .headline)
9
10let subtitleLabel = UILabel()
11subtitleLabel.text = "Pull to refresh or add a new item."
12subtitleLabel.font = .preferredFont(forTextStyle: .subheadline)
13subtitleLabel.textColor = .secondaryLabel
14subtitleLabel.numberOfLines = 0
15subtitleLabel.textAlignment = .center
16
17emptyView.addArrangedSubview(titleLabel)
18emptyView.addArrangedSubview(subtitleLabel)
19
20tableView.backgroundView = emptyView

This gives you space for a title, explanation, and even a retry button if the design calls for one.

Keep the Empty State Accessible

Empty-state text should use Dynamic Type friendly fonts and readable contrast. If the screen is actionable, the message should explain what the user can do next. A good empty state is not just decorative. It reduces confusion.

Consistency matters too. If every list in the app handles emptiness differently, the interface feels accidental. Reusing one small empty-state pattern across list screens improves the product feel immediately.

Common Pitfalls

A common mistake is setting the empty message once and forgetting to refresh it after network responses, filtering, or pull-to-refresh.

Another mistake is leaving separators visible behind the empty state, which makes the table look half-rendered.

It is also easy to blur empty and error states together. When loading fails, say that clearly instead of pretending there is simply no data.

Summary

  • Use tableView.backgroundView as the simplest empty-state mechanism.
  • Refresh the empty state every time the underlying data changes.
  • Distinguish no-data, no-results, and error scenarios with different messages.
  • Use a custom background view when a label is not enough.
  • Make the empty state accessible, clear, and intentional.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.