1

I'm trying to implement a tableView that has 4 different possible prototype cells. They all inherit from base UITableViewCell class and implement its protocol.

For two of the cells there's asynchronous data fetching but one in particular has been giving me fits. The flow is as follows:

1) Dequeue reusable cell

2) Call configure

func configure(someArguments: ) {
   //some checks
   process(withArguments: ) { [weak self in] in
      if let weakSelf = self {
         weakSelf.reloadDelegate.reload(forID: id)
      }
   }
}

3) If the async data is in the cache, configure the cell using the image/data/stuff available and be happy

4) If the async data is NOT in the cache, fetch it, cache it, and call the completion

func process(withArguments: completion:) {
    if let async_data = cache.exists(forID: async_data.id) {
        //set labels, add views, etc
    } else { 
       fetch_async_data() {
          //add to cache
          //call completion
       }
     } 
 }

5) If the completion is called, reload the row in question by passing the index path up to the UITableViewController and calling reloadRows(at:with:)

 func reload(forID: ) {
   tableView.beginUpdates()
   tableView.reloadRows(at: indexPath_matching_forID with: .automatic)
   tableView.endUpdates()
 }

Now, my understanding is that reloadRows(at:with:) will trigger another dataSource/delegate cycle and thus result in a fresh resuable cell being dequeued, and the configure method being called again, thereby making step #3 happy (the async data will now be in the cache since we just fetched it).

Except...that's not always happening. If there are cells in my initial fetch that require reloading, it works - they get the data and display it. Sometimes, though, scrolling down to another cell that requires fetching DOES NOT get the right data...or more specifically, it doesn't trigger a reload that populates the cell with the right data. I CAN see the cache being updated with the fresh data, but it's not...showing up.

If, however, I scroll completely past the bad cell, and then scroll back up, the correct data is used. So, what the hell reloadRows?!

I've tried wrapping various things in DispatchQueue.main.async to no avail.

reloadData works, ish, but is expensive because of potentially many async requests firing on a full reload (plus it causes some excessive flickering as cells come back)

Any help would be appreciated!

Nick Coelius
  • 4,668
  • 3
  • 24
  • 30
  • what exactly do you see in the cell you scroll to when "it doesn't trigger a reload that populates the cell with the right data"? Does it show some stale content from another index path? or is it just blank? – Leonid Usov Feb 14 '18 at 19:33
  • Stale content from another index path - So if in the first batch there's a cell with the same reuse type it'll show THAT cell's data in the later cell that doesn't repopulate. Again, I know the cache is being populated correctly (yay print statements) but the process(withArguments:) method is never called again so it can't fill in the right data. – Nick Coelius Feb 14 '18 at 19:41
  • In that case I'd look at your logic in `indexPath_matching_forID`. It could be that the reloading is done for an incorrect index path. How do you associate the id to the new index path? – Leonid Usov Feb 14 '18 at 19:52
  • In cellForRow, before I call configure, I add it to a dictionary `idToIndexPath[id] = indexPath` and I get the id by using `dataArray[indexPath.row]` (which returns an object which has an id on it) – Nick Coelius Feb 14 '18 at 19:57
  • Having printed out the indexPath being reloaded, I can visually confirm that the "right" indexPath is being used. – Nick Coelius Feb 14 '18 at 19:58
  • I added an edit, but just want to point out here - if I scroll past the cell and then up again, the correct data IS used. – Nick Coelius Feb 14 '18 at 20:03
  • Can you try commenting out the `beginUpdates` - `endUpdates` wrapping of the reload command? Additionally, please make sure that your completion is run on the UI thread – Leonid Usov Feb 14 '18 at 20:19
  • Well fuuuuuuuuuuck me...I guess I HADN'T tried DispatchQueue.main in all of the places - wrapping the completion call in a Dispatch makes it work juuuuust fine! Do you know why THAT would work, but not firing the completion on a background thread then reloadRows on Main? Also, make that a real response and I'll happily accept it as an answer! – Nick Coelius Feb 14 '18 at 21:10
  • Or maybe I spoke too soon - seems to not be working after all, there was just 1 false positive mixed in. – Nick Coelius Feb 14 '18 at 21:17

1 Answers1

0

Reused cells are not "fresh". Clear the cell while waiting for content.

func process(withArguments: completion:) {
    if let async_data = cache.exists(forID: async_data.id) {
        //set labels, add views, etc
    } else { 
       fetch_async_data() {
          // ** reset the content of the cell, clear labels etc ** 
          //add to cache
          //call completion
       }
    } 
 }
ozzieozumo
  • 426
  • 2
  • 8
  • I was previously doing that resetting in the `if it exists in the cache configure stuff...` block, because in theory if the call happens then I can just override the data before the use sees anything. Moving what little resetting I have to do to the async block shows the same result, except now there's simply no data being shown. The subsequent call to process() just ISN'T happening – Nick Coelius Feb 14 '18 at 20:14
  • Its unclear to me when/how you reset the id on a reused cell in the case of a cache miss. Where does "id" come from in the completion handler? Is it possible that your completion handler is referencing an old id from the reused cell? If so, your reload function likely would translate that old id into a non-visible indexPath, so the reloadRows call will do nothing (as you described). This combines what I pointed out above (dealing with reuse) with something mentioned by @LeonidUsov (logic in your id to indexPath translation). – ozzieozumo Feb 15 '18 at 00:20
  • In theory that shouldn't be an issue - the first time through cellForRow each row grabs a unique bit of data from my deserialized JSON and then takes that JSON's id and makes a key-value pair in a dictionary out of it. When I'm reloading the cells it uses id of the associated JSON data to get the index path out of the dictionary I made at the level of the tableViewcontroller – Nick Coelius Feb 15 '18 at 00:26
  • I've also littered `print` statements around and observed the that the correct indexPath is being passed to reloadRows as I scroll down in the simulator. – Nick Coelius Feb 15 '18 at 00:27
  • Yeah, I read your comments about the idToIndexPath dictionary. But what is "id" in the completion handler? weakSelf.reloadDelegate.reload(forID: id) – ozzieozumo Feb 15 '18 at 00:44
  • Each cell is configured with some data that makes up the underlying array for the `UITableView`. That configuration happens in cellForRow - so while I can see your concern that it might have the wrong JSON, I believe cellForRow SHOULD be called every time...indeed, I confirmed this with breakpoints and print statements, the right JSON blob is definitely being used. – Nick Coelius Feb 15 '18 at 01:32
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/165188/discussion-between-ozzieozumo-and-nick-coelius). – ozzieozumo Feb 15 '18 at 01:46