//
you're reading...

Programming

How to Maintain Loading State in Cells

When you have a table view containing lists of items coming from an API, often you need to display images coming from the network as well. Fetching these images could take a few seconds each, thus you’ll need to have a loading indicator as a placeholder. A static placeholder image won’t look as good as an animated one. Beside having it animated gives an indication to the user that it is being loaded – instead of outright missing.

Since table view cells are reused and table views usually keep a few cell instances off-screen, you’ll need to know when would these cells be displayed and when it gets off the screen. Animations take up CPU power (and battery as well), thus you’ll need to stop these placeholders’ animation as soon as the cells scroll off the screen.

You’ll probably thinking of keeping an in-memory dictionary to decide when to start or stop animating. But this is yet another data structure to maintain. Is there a better way?

The Solution

Thankfully there is. You can rely on the table view to let you know when to start animating a cell’s contents and when to stop. The table view would also know which cells that are currently visible.

How? Implement these two UITableViewDelegate callbacks to start or stop animating accordingly:

Thus your implementation of willDisplay: forRowAt: would only need to check whether the image for the cell is already cached in the file system. If it is, just load the image to the cell. If not, then activate the placeholder image’s animation. Consequentially the implementation of didEndDisplaying: forRowAt: would just check if the cell is showing an animated placeholder and just stop the animation to save CPU usage.

Have a look at the following playground code. This example shows a table view containing cells with UIActivityIndicator instances that animates only if its corresponding cell is on-screen.

import UIKit
import PlaygroundSupport

class ViewController: UITableViewController {

    let spinnerCellIdentifier = "spinnerCellIdentifier"

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(SpinnerTableViewCell.self, forCellReuseIdentifier: spinnerCellIdentifier)
    }

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

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: spinnerCellIdentifier, for: indexPath)
        cell.textLabel?.text = "indexPath: \(indexPath)"
        return cell
    }

    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if let spinnerCell = cell as? SpinnerTableViewCell {
            spinnerCell.spinner.startAnimating()
        }
    }

    override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if let spinnerCell = cell as? SpinnerTableViewCell {
            spinnerCell.spinner.stopAnimating()
        }
    }
}

class SpinnerTableViewCell: UITableViewCell {

    var spinner : UIActivityIndicatorView!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style:style, reuseIdentifier:reuseIdentifier)
        commonInit()
    }

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

    func commonInit() {
        let spinner = UIActivityIndicatorView(style:.medium)
        spinner.hidesWhenStopped = false
        self.spinner = spinner
        accessoryView = spinner
    }
}

PlaygroundPage.current.liveView = ViewController()

Of course you would need to have an asynchronous cache that loads the corresponding image in the background and store it – preferably the local caches folder.

Thus your network code can just focus on getting the images available as a local-file cache then notify the user interface that the image is available. Your user interface code would just need to reload the corresponding cells, after consulting the table view whether those cells are currently visible via indexPathsForVisibleRows.

You don’t even need to have any in-memory dictionary to map between cached files and data items – as long as you have a good convention on how you store the cached files. One way is for the data items shown in the list to have URLs to each image. In turn when those images gets downloaded, the cache files are named with UUIDs that are hashes of its source URL. These are UUID version 3 or version 5 having the URL namespace. It becomes straightforward to see whether an image is already cached or not – just create the UUID hash of its URL and see whether the corresponding cache file named as that UUID is present or not.

Next Steps

The best way to make knowledge stick is to apply it. My suggestion is to take the playground code above, run it, and then apply the same principle to a collection view. That is, create another playground page which shows a collection view with cells containing spinners. The excercise would be quite straightforward and probably take about half an hour.

UICollectionViewDelegate also declares similar methods that would be called back when a collection view cell is about to be shown on-screen and taken off-screen:

That’s all for now. Take care.



Do you enjoy this post? Enter your e-mail address in the form below to receive:

  • A cheat sheet on how to pass App Review Guideline 4.2 “Minimum Functionality”.
  • Notifications of new articles as soon as they are published.
  • Occasional tips and updates about my work.

You can unsubscribe any time and I won’t share your e-mail to any third party.

* indicates required

Discussion

No comments yet.

Leave a Reply