The aim of this post is showing how to fix choppy tableviews when they are showing huge images fetched from the cloud.
The App
The app is very simple, just a table view that shows a collection of pictures.
The app presents the following problems:
- The picture size is huge so the scroll must be smooth.
- There is a high memory consumption, so iOS quick the app out.
- The pictures are fetched every time that a cell is being presented (not cached).
- Fetch is not cancelled once the cell is not shown in the tableview due to scroll.
The picture resolution is 6144 x 4096.
The classical approach
Tableviews (and scrollviews) scroll must be smooth. User would not detect app bandwidth consumption easily, but a bumpy scroll is detected from the very beginning. Here what we have to do is to be sure that image is fetched in background and, once is retrieved, update image view in the main thread.
This is how a regular cell should have to be configured:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("ReusableCellId", forIndexPath: indexPath) if let cell = cell as? ImageTableViewCell, let _landscape:Landscape = self.landscapes![indexPath.row], let _url = NSURL(string: _landscape.url!){ cell.activityIndicator.hidden = true cell.activityIndicator.startAnimating() cell.imvLandscape.image = nil let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT dispatch_async(dispatch_get_global_queue(priority, 0)) { // do some task if let _data = NSData(contentsOfURL: _url){ dispatch_async(dispatch_get_main_queue()) { // update some UI cell.activityIndicator.hidden = false cell.activityIndicator.stopAnimating() cell.imvLandscape.image = UIImage(data: _data) } } } } return cell }
Memory consumption
But memory consumption increases in a dramatic (deadly) way:
When you access the NSData
, it is often compressed (with either PNG
or JPEG
). When you use the UIImage
, there is an uncompressed pixel buffer which is often 4 bytes per pixel (one byte for red, green, blue, and alpha, respectively). There are other formats, but it illustrates the basic idea, that the JPEG or PNG representations can be compressed, when you start using an image is uncompressed.
This model does not work at all with huge pictures, it is necessary to work with a different architecture.
The image provider
We will delegate the work of fetching and processing the image to a image provider class. Every cell will has its own image provider.
We will create the image provider in tableview willDisplayCell method:
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath){ guard let cell = cell as? ImageTableViewCell else { return } let landscape:Landscape = landscapes![indexPath.row] guard cache.objectForKey(landscape.url) != nil else { let imageProvider = ImageProvider( landscape: landscapes![indexPath.row],width: cell.imvLandscape.frame.size.width*2 ) { image in NSOperationQueue.mainQueue().addOperationWithBlock { self.cache.setObject(image!, forKey: landscape.url) if(self._isVisible(indexPath, tableView: tableView)){ cell.updateImageViewWithImage(image) } } return } imageProviders.insert(imageProvider) return } let image:UIImage = self.cache.objectForKey(landscape.url) as! UIImage cell.updateImageViewWithImage(image) }
It could be the case that cell will dissapear before image provider ends its tasks. You can cancel the operation done by image provider.
func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { guard let cell = cell as? ImageTableViewCell else { return } for provider in imageProviders.filter({ $0.landscape == cell.landscape }) { provider.cancel() imageProviders.remove(provider) } }
In my case I was not interested in doing that because it was highly likely to get back to the dissapeared cell.
Cache the downloaded stuff.
At the end is only necessary to download the image once, not every time that cell is going to be shown. We have implemented this by using NSCache object, this differs from other mutable collections in a few ways:
- It incorporates various auto-removal policies, which ensures that it does not use too much system memory.
- You can access from different threads without having to lock the cache yourselve.
- Retrieving something from an NSCache object returns an autoreleased result.
- Unlike an NSMutableDictionary object, a cache does not copy the key objects that are put into it.
Profile new architecture
With new architecture the memory consumption lasts in that way:
Awesome, It has been reduced memory consumption by 60!!!
Conclusion
There are times that is not under app developer the image size of images that has to fetch. If you use the classical approach then you will find that tableview scroll is choppy and most probaly OS will kick the app out. For avoiding such disgusting issue this architecture fits properly well. You can find the source code here.