0

During a recent interview, I was asked a scenario like #9 of these common interview questions regarding downloading images asynchronously into a table view cell. I understand the necessity for it to be called in cellForIndexPath and asynchronously but I was stumped as to how to check to see if the cell is still in view after the async call is complete (see the bullet #3 excerpt below). In other words, after an async call, how can I determine whether the table cell I was fetching data for is still in the view.

When the image has downloaded for a cell we need to check if that cell is still in the view or whether it has been re-used by another piece of data. If it’s been re-used then we should discard the image, otherwise we need to switch back to the main thread to change the image on the cell.

vikingosegundo
  • 52,040
  • 14
  • 137
  • 178
galenom
  • 89
  • 8
  • You can assign tag to the cell and then check if the cell with that tag is present in the tableView or not. You can get visible cells easily as UITableView provides method for it. – Vishal Sonawane Feb 22 '17 at 08:33
  • Use some great async library like AlamofireImage/ Kingfisher etc. which will take care of your async image download. On another hand attach some tag for your cell or you can check for indexpath.row to get the visible cell and implement your image logic there. – onCompletion Feb 22 '17 at 08:35

2 Answers2

2

You should start downloading your image in the background with a callback mechanism that can decide if the image should still be displayed after it's been loaded.

One option would be to subclass UIImageView or UITableViewCell and store a reference to the NSURL of the image. Then, when your callback is called, you could check if the image view or the cell's cached URL is the one of the image you have, and decide to display it or not.

I wouldn't recommend on:

  • relying on a view's tag as it requires some sort of association table between a NSURL and an integer, which requires a manager object and is not helping reusability of your code
  • relying on the cell's indexPath as updates of the table or cells being reused for other index paths could occur while the network request happened

A more advanced options is described in Associated Objects, by NSHipster:

When extending the behavior of a built-in class, it may be necessary to keep track of additional state. This is the textbook use case for associated objects. For example, AFNetworking uses associated objects on its UIImageView category to store a request operation object, used to asynchronously fetch a remote image at a particular URL.

Mick F
  • 7,312
  • 6
  • 51
  • 98
  • Thanks for the answer @DirtyHarry . Your solution is similary to what I had in mind when brainstorming solutions for this question. Are you able to take a look at my [example code](http://pastebin.com/EtwWxGN4) on Pastebin please? I don't think the code inside the callback is correct. What necessary change do I have to make to get the visible cell compare the cell's cached url with the image's url? – galenom Feb 25 '17 at 07:28
  • I think your code is alright. Maybe just call cell.setNeedsLayout() to force the cell to be drawing the image with the right aspect. I set up some demo code here that works fine: https://github.com/Bootstragram/Martinet/blob/master/Example/Martinet/DemoAsyncDownloadsPresenter.swift – Mick F Feb 27 '17 at 16:45
  • Marked as correct. Another solution using tableviews instance method [cellForRow(at:)](https://developer.apple.com/reference/uikit/uitableview/1614983-cellforrow). Inside the async block, I get cell with active indexPath. If returns nil, cell for that index path is not in view so I don't set the image. `DispatchQueue.main.async { if let newCell = self.tableView.cellForRow(at: indexPath) as? ContactCell { newCell.imageView?.image = image newCell.layoutSubviews() } }` – galenom Mar 06 '17 at 07:51
1

You can simply check whether the UITableViewCell is still in the view or not by using the following method of UITableView:

// return indexPaths that are visible

var indexPathsForVisibleRows: [IndexPath]?

Not to check whether to reload a specific row or not, you can do it by using the following method:

func downloadImageForCell(indexPath: IndexPath) {

    // Asynchronous download method here

    // After download is completed. Call the below in mainqueue
    if let indexPaths:[IndexPath] = self.tableView.indexPathsForVisibleRows {
       // the above line checks if indexPath is available
       if indexPaths.contains(indexPath) {
           self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
       }
    }
}

Please let me know if you have any problems in implementing this code

KrishnaCA
  • 5,615
  • 1
  • 21
  • 31
  • Warning: if the table gets updated during the network request, the indexPath of the cell might have changed. So concurrent calls for the same indexPath might happen, and if the 1st request takes longer than the 2nd, you will end up with the wrong image being displayed. – Mick F Feb 22 '17 at 09:03
  • @DirtyHenry, the current scenario clearly states that he is downloading images for the `UITableViewCell`. That means the current network call is for updating the images in the already fetched data. So, I believe that this is a safe bet for the current scenario – KrishnaCA Feb 22 '17 at 09:06
  • After a closer look, I still don't think it is. What if the user scrolls the table view and the cell get reused for another indexPath ? Then `self.tableView.indexPath(cell: cell)` might return an indexPath that is not where you want your image to end up in. – Mick F Feb 22 '17 at 09:11
  • @DirtyHenry, thanks for the discussion. You're right about the possibility `cell reusability giving wrong indexPath value`. That simply implies that `indexPath(for cell: UITableViewCell)` method is not usable for the case. I believe `indexPathsForVisibleRows` is a better bet considering that it doesn't get affected by cell reusability. Considering that, I modified my answer accordingly – KrishnaCA Feb 22 '17 at 09:30