UITableView
iOS Development
Swift
cellForRowAtIndexPath
Xcode Debugging

cellForRowAtIndexPath not called

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When cellForRowAt (or cellForRowAtIndexPath: in Objective-C) is not called, the table view has no cells to display. The most common causes are: numberOfRowsInSection returns 0 (empty data source), the dataSource delegate is not set, the table view has zero height due to Auto Layout constraints, or reloadData is called before the data is loaded. Debugging this requires checking the data source methods, the delegate connection, and the table view's frame in the view hierarchy.

Cause 1: numberOfRowsInSection Returns 0

swift
1class ViewController: UIViewController, UITableViewDataSource {
2    var items: [String] = []  // Empty array!
3
4    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
5        return items.count  // Returns 0 — no cells will be created
6    }
7
8    func tableView(_ tableView: UITableView,
9                    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
10        // Never called because numberOfRowsInSection returns 0
11        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
12        cell.textLabel?.text = items[indexPath.row]
13        return cell
14    }
15}
swift
1// FIX: Load data and call reloadData
2override func viewDidLoad() {
3    super.viewDidLoad()
4    tableView.dataSource = self
5
6    // Load data asynchronously
7    fetchItems { [weak self] loadedItems in
8        DispatchQueue.main.async {
9            self?.items = loadedItems
10            self?.tableView.reloadData()  // Now numberOfRowsInSection returns > 0
11        }
12    }
13}

If numberOfRowsInSection returns 0, UIKit never calls cellForRowAt. Add a breakpoint or print statement in numberOfRowsInSection to verify.

Cause 2: dataSource Not Set

swift
1// BROKEN: dataSource not connected
2class ViewController: UIViewController {
3    @IBOutlet weak var tableView: UITableView!
4
5    override func viewDidLoad() {
6        super.viewDidLoad()
7        // Missing: tableView.dataSource = self
8    }
9}
10
11// FIX: Set dataSource in code
12override func viewDidLoad() {
13    super.viewDidLoad()
14    tableView.dataSource = self
15    tableView.delegate = self
16}
17
18// Or connect in Interface Builder:
19// Control-drag from UITableView to ViewController for dataSource and delegate

Without a dataSource, UIKit does not know which object provides the cells. The table view appears empty with no errors.

Cause 3: Table View Has Zero Height

swift
1// Auto Layout constraints give the table view zero height
2// Check in the debugger:
3override func viewDidAppear(_ animated: Bool) {
4    super.viewDidAppear(animated)
5    print("TableView frame:", tableView.frame)
6    // If height is 0, cellForRowAt is never called
7}
swift
1// FIX: Ensure proper Auto Layout constraints
2// In Interface Builder or code:
3tableView.translatesAutoresizingMaskIntoConstraints = false
4NSLayoutConstraint.activate([
5    tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
6    tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
7    tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
8    tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
9])

If the table view's height is 0 (due to missing or conflicting constraints), UIKit determines that zero cells fit on screen and never calls cellForRowAt. Use the Debug View Hierarchy in Xcode to inspect the table view's frame.

Cause 4: reloadData Called on Background Thread

swift
1// BROKEN: reloadData on background thread — UI may not update
2URLSession.shared.dataTask(with: url) { data, _, _ in
3    self.items = parseJSON(data!)
4    self.tableView.reloadData()  // ❌ Wrong thread!
5}.resume()
6
7// FIX: Always reload on main thread
8URLSession.shared.dataTask(with: url) { data, _, _ in
9    let parsed = parseJSON(data!)
10    DispatchQueue.main.async {
11        self.items = parsed
12        self.tableView.reloadData()  // ✅ Main thread
13    }
14}.resume()

UIKit is not thread-safe. Calling reloadData from a background thread may silently fail or cause intermittent issues where cellForRowAt is not called.

Cause 5: Cell Identifier Not Registered

swift
1// BROKEN: cell identifier not registered
2func tableView(_ tableView: UITableView,
3                cellForRowAt indexPath: IndexPath) -> UITableViewCell {
4    let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath)
5    // Crashes if "MyCell" is not registered — this is a crash, not "not called"
6    return cell
7}
8
9// FIX: Register the cell
10override func viewDidLoad() {
11    super.viewDidLoad()
12    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "MyCell")
13    // Or register a nib:
14    tableView.register(UINib(nibName: "CustomCell", bundle: nil),
15                       forCellReuseIdentifier: "MyCell")
16}

An unregistered cell identifier causes a crash, not a silent failure. But if the crash is caught silently or occurs in a test environment, it can appear as if cellForRowAt is not called.

Cause 6: numberOfSections Returns 0

swift
1// Easy to miss: numberOfSections returns 0
2func numberOfSections(in tableView: UITableView) -> Int {
3    return 0  // No sections = no rows = no cells
4}
5
6// FIX: Return at least 1
7func numberOfSections(in tableView: UITableView) -> Int {
8    return 1  // Default if not implemented
9}

If you implement numberOfSections and it returns 0, no cells are displayed. If you do not implement it, UIKit defaults to 1 section.

Debugging Checklist

swift
1override func viewDidLoad() {
2    super.viewDidLoad()
3
4    // 1. Verify dataSource is set
5    print("dataSource:", tableView.dataSource as Any)
6
7    // 2. Verify data is populated
8    print("items count:", items.count)
9
10    // 3. Verify table view has nonzero frame
11    print("frame:", tableView.frame)
12}
13
14func tableView(_ tableView: UITableView,
15                numberOfRowsInSection section: Int) -> Int {
16    let count = items.count
17    print("numberOfRowsInSection:", count)  // Should be > 0
18    return count
19}
20
21func tableView(_ tableView: UITableView,
22                cellForRowAt indexPath: IndexPath) -> UITableViewCell {
23    print("cellForRowAt:", indexPath)  // If this never prints, check above
24    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
25    cell.textLabel?.text = items[indexPath.row]
26    return cell
27}

Common Pitfalls

  • Data loads after viewDidLoad but reloadData is never called: If data is fetched asynchronously, the initial numberOfRowsInSection returns 0 (empty array). You must call tableView.reloadData() after the data arrives to trigger a new layout pass.
  • Table view hidden behind another view: If another view is placed on top of the table view or the table view's isHidden property is true, cells are still created but not visible. Use Xcode's Debug View Hierarchy to check the view stack.
  • Using UITableViewController but overriding loadView: UITableViewController automatically creates and configures a UITableView with itself as the data source. Overriding loadView without calling super breaks this automatic setup.
  • Forgetting UITableViewDataSource protocol conformance: In Swift, the class must declare conformance to UITableViewDataSource. Without it, setting tableView.dataSource = self causes a compiler error (or a runtime crash if forced with casting).
  • Storyboard prototype cell identifier mismatch: If the cell identifier in dequeueReusableCell(withIdentifier:) does not match the identifier set in the Storyboard's prototype cell, the table view crashes. Double-check the identifier spelling and case.

Summary

  • cellForRowAt is not called when numberOfRowsInSection returns 0 — verify your data array is populated
  • Ensure dataSource is connected (in code or Interface Builder)
  • Check the table view has nonzero height via Auto Layout constraints
  • Always call reloadData() on the main thread after async data loads
  • Use print statements or breakpoints in numberOfRowsInSection to confirm it returns > 0
  • Use Xcode's Debug View Hierarchy to verify the table view's frame and visibility

Course illustration
Course illustration

All Rights Reserved.