UITableView Async image not always correct

334 views Asked by At

I have a UITableView and during the initial loading of my app it sends multiple API requests. As each API request returns, I add a new row to the UITableView. So the initial loading adds rows in random orders at random times (Mostly it all happens within a second).

During cell setup, I call an Async method to generate an MKMapKit MKMapSnapshotter image.

I've used async image loading before without issue, but very rarely I end up with the image in the wrong cell and I can't figure out why.

I've tried switching to DiffableDataSource but the problem remains.

In my DiffableDataSource I pass a closure to the cell that is called when the image async returns, to fetch the current cell in case it's changed:

let dataSource = DiffableDataSource(tableView: tableView) {
            (tableView, indexPath, journey) -> UITableViewCell? in
            
            let cell = tableView.dequeueReusableCell(withIdentifier: "busCell", for: indexPath) as! JourneyTableViewCell
            cell.setupCell(for: journey) { [weak self] () -> (cell: JourneyTableViewCell?, journey: Journey?) in
                
                if let self = self
                {
                    let cell = tableView.cellForRow(at: indexPath) as? JourneyTableViewCell
                    let journey = self.sortedJourneys()[safe: indexPath.section]
                    return (cell, journey)
                }
                return(nil, nil)
            }
            
            return cell
        }

Here's my cell setup code:

override func prepareForReuse() {
    super.prepareForReuse()
    
    setMapImage(nil)
    journey = nil
    asyncCellBlock = nil
}

func setupCell(for journey:Journey, asyncUpdateOriginalCell:@escaping JourneyOriginalCellBlock) {
    
    self.journey = journey
    
    // Store the async block for later
    asyncCellBlock = asyncUpdateOriginalCell
    
    // Map
    if let location = journey.location,
       (CLLocationCoordinate2DIsValid(location.coordinate2D))
    {
        // Use the temp cached image for now while we get a new image
        if let cachedImage = journey.cachedMap.image
        {
            setMapImage(cachedImage)
        }
        
        // Request an updated map image
        journey.createMapImage {
            [weak self] (image) in
            
            DispatchQueue.main.async {
                
                if let asyncCellBlock = self?.asyncCellBlock
                {
                    let asyncResult = asyncCellBlock()
                    if let cell = asyncResult.cell,
                       let journey = asyncResult.journey
                    {
                        if (cell == self && journey.id == self?.journey?.id)
                        {
                            self?.setMapImage(image)
                            
                            // Force the cell to redraw itself.
                            self?.setNeedsLayout()
                        }
                    }
                }
            }
        }
    }
    else
    {
        setMapImage(nil)
    }
}

I'm not sure if this is just a race condition with the UITableView updating several times in a small period of time.

2

There are 2 answers

1
Rahul Dasgupta On

I think this is because when the image is available then that index is not there. Since the table view cells are reusable, it loads the previous image since the current image is not loaded yet.

if let cachedImage = journey.cachedMap.image
    {
        setMapImage(cachedImage)
    }
else {
        // make imageView.image = nil
}
5
Mat On

I can see you already cache the image but I think you should prepare the cell for reuse like this:

override func prepareForReuse() {
    super.prepareForReuse()
    let image = UIImage()
    self.yourImageView.image = image
    self.yourImageView.backgroundColor = .black
}