Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITableViewCell asynchronously loading images issue - Swift

In my app, I built my own asynchronous image loading class. I pass in a object, then it checks if the cache (NSCache) has the image, if not it will then check the file system if the image is saved already. If the image is not saved already, it will then download the image in the background (NSOperations help).

This works great so far, but I have ran into a few small issues with the table view loading the images.

First off, this is the function I use to set up the table view cell fromtableView(tableView:, willDisplayCell:, forRowAtIndexPath:)

func configureCell(cell: ShowTableViewCell, indexPath: NSIndexPath) {

    // Configure cell
    if let show = dataSource.showFromIndexPath(indexPath) {

        ImageManager.sharedManager.getImageForShow(show, completionHandler: { (image) -> Void in
            if self.indexPathsForFadedInImages.indexOf(indexPath) == nil {
                self.indexPathsForFadedInImages.append(indexPath)

                if let fetchCell = self.tableView.cellForRowAtIndexPath(indexPath) as? ShowTableViewCell {
                    func fadeInImage() {
                        // Fade in image
                        fetchCell.backgroundImageView!.alpha = 0.0
                        fetchCell.backgroundImage = image
                        UIView.animateWithDuration(showImageAnimationSpeed, animations: { () -> Void in
                            fetchCell.backgroundImageView!.alpha = 1.0
                        })
                    }

                    if #available(iOS 9, *) {
                        if NSProcessInfo.processInfo().lowPowerModeEnabled {
                            fetchCell.backgroundImage = image
                        }
                        else {
                            fadeInImage()
                        }
                    }
                    else {
                        fadeInImage()
                    }
                }
                else {
                    // Issues are here
                }
            }
            else {
                // Set image
                cell.backgroundImage = image
            }
        })
...
}

Where "// Issues are here" comment is, that is where I run into multiple issues.

So far, I have not figured out another way to validate that the image belongs to the cell for sure where "// Issues are here" is. If I add

cell.backgroundImage = image

there, then it fixes the issue where sometimes the image will not display on the table view cell. So far the only cause I have found for this is that the image is being returned faster than I can return the table view cell so that is why the table view says there is not a cell at that index path.

But if I add that code there, then I run into another issue! Cells will display the wrong images and then it lags down the app and the image will constantly switch, or even just stay on the wrong image.

I have checked that it runs on the main thread, image downloading and caching is all fine. It just has to do that the table is saying there is no cell at that index path, and I have tried getting an indexPath for the cell which returns also nil.

A semi-solution to this problem is called tableView.reloadData() in viewWillAppear/viewDidAppear. This will fix the issue, but then I lose the animation for table view cells on screen.

EDIT:

If I pass the image view into getImageForShow() and set it directly it will fix this issue, but that is less ideal design of code. The image view obviously exists, the cell exists, but for some reason it doesn't want to work every time.

like image 916
Maximilian Litteral Avatar asked Mar 14 '23 21:03

Maximilian Litteral


1 Answers

Table views reuse cells to save memory, which can cause problems with any async routines that need to be performed to display the cell's data (like loading an image). If the cell is supposed to be displaying different data when the async operation completes, the app can suddenly go into an inconsistent display state.

To get around this, I recommend adding a generation property to your cells, and checking that property when the async operation completes:

protocol MyImageManager {
    static var sharedManager: MyImageManager { get }
    func getImageForUrl(url: String, completion: (UIImage?, NSError?) -> Void)
}

struct MyCellData {
    let url: String
}

class MyTableViewCell: UITableViewCell {

    // The generation will tell us which iteration of the cell we're working with
    var generation: Int = 0

    override func prepareForReuse() {
        super.prepareForReuse()
        // Increment the generation when the cell is recycled
        self.generation++
        self.data = nil
    }

    var data: MyCellData? {
        didSet {
            // Reset the display state
            self.imageView?.image = nil
            self.imageView?.alpha = 0
            if let data = self.data {
                // Remember what generation the cell is on
                var generation = self.generation
                // In case the image retrieval takes a long time and the cell should be destroyed because the user navigates away, make a weak reference
                weak var wcell = self
                // Retrieve the image from the server (or from the local cache)
                MyImageManager.sharedManager.getImageForUrl(data.url, completion: { (image, error) -> Void in
                    if let error = error {
                        println("There was a problem fetching the image")
                    } else if let cell = wcell, image = image where cell.generation == generation {
                        // Make sure that UI updates happen on main thread
                        dispatch_async(dispatch_get_main_queue(), { () -> Void in
                            // Only update the cell if the generation value matches what it was prior to fetching the image
                            cell.imageView?.image = image
                            cell.imageView?.alpha = 0
                            UIView.animateWithDuration(0.25, animations: { () -> Void in
                                cell.imageView?.alpha = 1
                            })
                        })
                    }
                })
            }
        }
    }
}

class MyTableViewController: UITableViewController {

    var rows: [MyCellData] = []

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("Identifier") as! MyTableViewCell
        cell.data = self.rows[indexPath.row]
        return cell
    }

}

A couple other notes:

  • Don't forget to do your display updates on the main thread. Updating on a network activity thread can cause the display to change at a seemingly random time (or never)
  • Be sure to weakly reference the cell (or any other UI elements) when you're performing an async operation in case the UI should be destroyed before the async op completes.
like image 148
Dan Nichols Avatar answered Apr 01 '23 03:04

Dan Nichols