The Most Common Mistake In Using UITableViews

UITableView

UITableView is one of the most important user interface objects you are using in iOS development. However, there is one common mistake in using UITableViews that could drive you crazy.

Hint: This post has been updated to Swift 3 and Xcode 8

Example

Take a look at the following example:

import UIKit

class MainTableViewController: UITableViewController {
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        return 20
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCellIdentifier", for: indexPath)
        
        if indexPath.row % 2 == 0 {
            cell.imageView?.image = UIImage(named: "\(indexPath.row)")
        } else {
            cell.textLabel?.text = "\(indexPath.row)"
        }
        
        return cell
    }
    
}

So basically we are displaying alternately an image and a text label. The result looks like this:

Screenshot01

Everything seems to be fine.

The Problem

However, if we scroll down, we’ll see that something is going wrong:

Screenshot02

And if we scroll up again, the problem will arise as well:

Screenshot03

So what’s going on here? The answer is relatively simple: We are recycling the cells by using the method dequeueReusableCellWithIdentifier. And you should recycle cells because the allocation of views need a lot of resources. So let’s take a look at the following picture to understand what’s happening:

ReusedCells

Let’s take a look at row no. 12. Since 12%2 = 0, the picture with number 12 should be displayed. In fact that happens, but also the label with the number 1 is displayed! That happens because the cell from row 1 is recycled. And since we are not assigning an empty value to the label, you can still see the 1.

Next row is similiar: It’s the row with the no. 13, so the label with the text 13 is displayed. However, the cell from row no 0 is recycled and since where are not removing the image, it is still displayed.

You can also see that the behaviour is not very predictable and that sometimes everything seems to be working fine. So every time you are scrolling the result will be different. Depending on the circumstances it could happen that this issue doesn’t happen very often, so that it is very hard to reproduce the bug. But it’s very common to make this mistake, even for more experienced iOS developers.

The Solution

In order to solve the problem we just have to assign all properties all the time:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
    let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCellIdentifier", for: indexPath)
        
    if indexPath.row % 2 == 0 {
        cell.imageView?.image = UIImage(named: "\(indexPath.row)")
        cell.textLabel?.text = nil
    } else {
        cell.imageView?.image = nil
        cell.textLabel?.text = "\(indexPath.row)"
    }
        
    return cell
}

Now we assign nil, if the text or the image shouldn’t be displayed. And if we start the app and scroll down, we’ll get the following result:

Screenshot05

[thrive_text_block color=”blue” headline=”Conclusion”] If table view cells gets recycled and you don’t pay enough attention, some weird bugs can happen. But if you are always following the rule to always assign all properties all the time, nothing can go wrong.[/thrive_text_block]

Resources

Title Image: @ igor.stevanovic / shutterstock.com
UITableView Tutorials

6 comments

  1. If you create a custom cell, you can just set the image view image to nil in its prepareForReuse override.

  2. Well, this would work fine when the tableview cell has minimal properties. But I would create a custom tableview cell and override prepareForReuse method and set all the properties to nil there. So I have a cleaner cellForRowAtIndexPath.

  3. You can also use Numberofrows property of tableview and remove the callback method if number of rows are fixed.

Comments are closed.