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.
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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With