UITableView Loading Images Slowly - uitableview

The TableView fetches images from parse api. However, when scrolling through the tableView, there is lag while the image is loading.
Is there is an efficient way to decrease lag? Some way to load images before (or even after) the user scrolls to the cell? I'm not too familiar with dealing with asynchronous calls.
Fetch Image Function
func fetchImage(restaurantArray: PFObject!, completionHandler: ImageCompletionHandler!){
var imageReference = restaurantArray["PhotoUploaded"] as PFFile
imageReference.getDataInBackgroundWithBlock{
(data, error) -> Void in
if (error != nil){
completionHandler(image: nil, error: error)
}else{
let image = UIImage(data: data)
completionHandler(image: image, error: nil)
}
}
}
TableView Cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as FeedCell
cell.nameLabel.text = restaurantNames[indexPath.row]
// check if image is empty
if foodPhotoObjects.isEmpty {
} else {
self.fetchImage(self.foodPhotoObjects[indexPath.row] as? PFObject, completionHandler: {
(image, error) -> () in
if image != nil {
cell.mainRestaurantImageView.image = image
cell.mainRestaurantImageView.contentMode = .ScaleAspectFill
cell.mainRestaurantImageView.clipsToBounds = true
}else{
//alert user: no image or put placeholder image
}
})
}
return cell
}

You can get this functionality for free by using PFImageView, less code for you to write to achieve the desired effect. Just call loadInBackground: after setting the file property.
https://parse.com/docs/ios/api/Classes/PFImageView.html
As for UI lag, profile in Instruments to determine what is so expensive that it is causing significant noticeable lag.

A couple of thoughts:
You should confirm whether this completion block is running on the main thread (examine NSThread.isMainThread) and if not, make sure to dispatch the update of the image view back to the main thread, e.g. dispatch_async(dispatch_get_main_queue()) { ... }. Trying to perform updates of UIKit controls from a background thread can end up with inconsistent behavior, so make sure you're only updating the UI form the main thread.
You should consider the implications of the user quickly scrolling down and back up in the table view. Your code will result in re-fetching images previously retrieved.
Sometimes the underlying NSURLCache will handle this gracefully, caching the images for you. In other cases it will not. (Sadly, the caching of network requests is dictated by some fairly opaque criteria, and depending upon the server implementation, you might not have a lot of control over that.)
You might want to confirm your behavior, and implement your own caching (e.g. NSCache) of previously downloaded images and checking that before retrieving images again.
I would make sure to test this process in conjunction with the network link conditioner so you can confirm the behavior of the app in less than ideal environments (i.e. real-world situations).
Unrelated to your question there are two issues with updating the cell asynchronously:
If the cell scrolled out of view by the time the image retrieval completed, the cell could have been reused, and the cell may have been reused for another row in the table. (Test this by running the app with the network link conditioner degrading the network response time to something more lke a poor cellular connector.) This problem can manifest itself as a "flickering" of images or replacing images with images associated with other cells.
You generally solve this by calling tableView.cellForRowAtIndexPath(indexPath) (a UITableView method, not to be confused with the similarly named UITableViewDataSource method), which will return nil if this index path has scrolled off of the screen, and will return the UITableViewCell if the cell is visible. Use this rather than the previously established cell value when doing the final asynchronous population of the cell.
If there is any possibility that the table could have been reloaded (i.e. the data source changed) during the process of retrieving the image asynchronously, you might actually want to go back to the model, identifying the appropriate index path associated with the image you just downloaded asynchronously.
Bottom line, imagine some degenerate situation where it took a minute or so to retrieve the image (unlikely, just imagine). Think through the contingencies associated with that (the cell scrolled off and subsequently reused, the indexPath for the image might not longer be valid, etc.). Make sure you handle these contingencies gracefully.

Related

Problems with asynchronous data, UITableView, and reloadRowsAt

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!
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
}
}
}

Asynchronous image loading from Core Data in Swift

I am building an application which uses a Core Data database to store its data on products. These products are displayed in a UICollectionView. Each cell in this collection view displays basic information on the product it contains, including an image.
Although the cells are relatively small, the original images they display are preferably quite large as they should also be able to be displayed in a larger image view. The images are loaded directly from Core Data in my CellForItemAtIndexPath: Method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionWineCell
var current: Product!
//product details are set on labels
//image is set
if current.value(forKey: "image") != nil {
let image = current.value(forKey: "image") as! WineImage
let loadedImage = UIImage(data: image.image)
cell.imageview.image = loadedImage
} else {
cell.imageview.image = UIImage(named: "ProductPlaceholder.png")
}
return cell
}
When the collection of products grows, scrolling gets bumpier and a lot of frames are dropped. This makes sense to me, but so far I haven't found a suitable solution. When looking online a lot of documentation and frameworks are available for asynchronous image loading from a URL (either online or a file path), but doing this from Core Data does not seem very common.
I have already tried doing it using an asynchronous fetch request:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName:"ProductImage")
fetchRequest.predicate = NSPredicate(format: "product = %#", current)
let asyncRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { results in
if let results = results.finalResult {
let result = results[0] as! ProductImage
let loadedImage = UIImage(data: result.image)
DispatchQueue.main.async(execute: {
cell.wineImage.image = loadedImage
})
}
}
_ = try! managedObjectContext.executeRequest(asyncRequest)
However, this approach does not seems to smoothen things out either
QUESTION
When displaying large sets of data, including images, from Core Data, how does one load images in a way that it does not cause lags and frame drops in a UICollectionView?
If the images can be, as you say, quite large, a better approach is not to save them in Core Data but to put them in files. Store the filename in Core Data and use that to look up the file.
But that's not the immediate problem. Even with that you'll get slowdowns from spending time opening and decoding image data. A better approach is, basically, don't do that. In your collection views the images are probably displayed much smaller than their full size. Instead of using the full size image, generate a thumbnail at a more appropriate size and use that in the collection view. Do the thumbnail generation whenever you first get the image, whether from a download or from the user's photo library or wherever. Keep the thumbnail for use in the collection view. Only use the full size image when you really need it.
There are many examples online of how to scale images, so I won't include that here.

Why UITableViewCell displays previous value at first and just later updates self?

I have a UITableView with a UIImageView on it. On these imageViews I'm downloading images from the server and display them. The problem is that when I scroll my UITableView, at first it shows another cell's image and just later(in 2-3 seconds) it updates the cell's imageView to the properly image.
How can I fix it? I've tried to use background and main thread mix, like that:
extension UIImageView {
func getDataFromUrl(url:String, completion: ((data: NSData?) -> Void)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data, response, error) in
completion(data: NSData(data: data!))
}.resume()
})
}
func downloadImage(url:String){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
self.getDataFromUrl(url) { data in
dispatch_async(dispatch_get_main_queue()) {
self.contentMode = UIViewContentMode.ScaleAspectFill
self.image = UIImage(data: data!)
}
}
})
}
}
and use these extension on my cellForRowAtIndexPath as:
cell.coverImage.downloadImage("\(credentials.postCoverURL)\(recentNews[indexPath.row].image)")
but it doesn't help me. It works as previously. What I do wrong? If you can, please give a description(explanation) to your answer. It will be more useful! Thanks!
UPDATE
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: newsCell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! newsCell
cell.coverImage.downloadImage("\(credentials.postCoverURL)\(recentNews[indexPath.row].image)")
return cell
}
What is actually happening in your code is that all cells are given a reuse identifier, meaning that they all get put into the same object pool. This means that when you use [tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier]; that you are retrieving a cell from this one pool. It already has a image set up which you need to clear.
Before setting up the cell to download Image you need to clear the image before.
cell.coverImage.image = nil
cell.coverImage.downloadImage("\(credentials.postCoverURL)\(recentNews[indexPath.row].image)")
Let me explain the problem.
Imagine you have the first row, i call it ROW_A object. ROW_A should download and display image with URL_1, by using a thread THREAD_1. When it's in you cellForRow function, you started THREAD_1 to download the image for ROW_A
When you scroll, the first row become invisible, so the ROW_A view object will be reused for displaying another row, for example, row 11th. In the cellForRow, the same process as before, you started thread THREAD_11 to download the image with URL_11 and display that image using ROW_A object (as i said before, the ROW_A object is reused)
The problem is, after a long time downloading IMAGE_1 from URL_1, THREAD_1 finishes its works and set IMAGE_1 to ROW_A. You will see IMAGE_1 being showed.
After that, THREAD_11 finishes too, it set IMAGE_11 to the ROW_A. But ROW_A is displaying IMAGE_1, so that why you see IMAGE_1 then IMAGE_11 on row 11th.
To fix this problem, you should check the image view to see if it's assigned to other downloading thread. I mean, if THREAD_1 finish, THREAD_1 should know that ROW_A is not belong to THREAD_1 anymore. You can create a UIImageView subclass with a url:String property, then when the downloading thread finishes, you compare the url string of downloaded image with value of url property, if it's different, don't show the image.
Another option is using AFNetworking for downloading and showing the remote images. That's very simple and you can find many tutorial on Google.

How to create UITableViewCells with complex content in order to keep scrolling fluent

I am working on a project where I have a table view which contains a number of cells with pretty complex content. It will be between usually not more than two, but in exceptions up to - lets say - 30 of them. Each of these complex cells contain a line chart. I am using ios-charts (https://github.com/danielgindi/ios-charts) for this.
This is what the View Controller's content looks like:
The code I use for dequeuing the cells in the viewController's cellForRowAtIndexPath method is kind of the following:
var cell = tableView.dequeueReusableCellWithIdentifier("PerfgraphCell", forIndexPath: indexPath) as? UITableViewCell
if cell == nil {
let nib:Array = NSBundle.mainBundle().loadNibNamed("PerfgraphCell", owner: self, options: nil)
cell = nib[0] as? FIBPerfgraphCell
}
cell.setupWithService(serviceObject, andPerfdataDatasourceId: indexPath.row - 1)
return cell!
and in the cell's code I have a method "setupWithService" which pulls the datasources from an already existing object "serviceObject" and inits the drawing of the graph, like this:
func setupWithService(service: ServiceObjectWithDetails, andPerfdataDatasourceId id: Int) {
let dataSource = service.perfdataDatasources[id]
let metadata = service.perfdataMetadataForDatasource(dataSource)
if metadata != nil {
if let lineChartData = service.perfdataDatasourcesLineChartData[dataSource] {
println("starting drawing for \(lineChartData.dataSets[0].yVals.count) values")
chartView.data = lineChartData
}
}
}
Now the problem: depending on how many values are to be drawn in the chart (between 100 and 2000) the drawing seems to get pretty complex. The user notices that when he scrolls down: as soon as a cell is to be dequeued that contains such a complex chart, the scrolling gets stuck for a short moment until the chart is initialized. That's of course ugly!
For such a case, does it make sense to NOT dequeue the cells on demand but predefine them and hold them in an array once the data that is needed for graphing is received by the view controller and just pull the corresponding cell out of this array when it's needed? Or is there a way to make the initialization of the chart asynchronous, so that the cell is there immediately but the chart appears whenever it's "ready"?
Thanks for your responses!
What you're trying to do is going to inevitably bump into some serious performance issues in one case or another. Storing all cells (and their data into memory) will quickly use up your application's available memory. On the other hand dequeueing and reloading will produce lags on some devices as you are experiencing right now. You'd be better off by rethinking your application's architecture, by either:
1- Precompute your graphs and export them as images. Loading images into and off cells will have much less of a performance knockoff.
2- Make the table view into a drill down menu where you only show one graph at a time.
Hope this helps!

Delete dynamic cell in GetCell method

I am trying trying to remove a cell from the UITableView if the image for the cell is a bad image. Basically my code does a threadPool call for each cell's image to make the flow of binding the data as a user scroll smooth as so inside the GetCell method:
public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
//other stuff
ThreadPool.QueueUserWorkItem(new WaitCallback(RetrieveImage),(object)cell);
}
within that asynchronous method I call the cell.imageUrl property and bind it to an NSData object as so:
NSData data = NSData.FromUrl(nsUrl); //nsUrl is cell.imageUrl
From there I know that if data==null then there was a problem retrieving the image so I want to remove it. What I currently do is set the hidden property to true, but this leaves a blank space. I want to remove the cell entirety so the user won't even know it existed.
I can't set the cell height to 0 because I don't know if the imageUrl is bad until that indexrowpath initiates getcell call. I don't want to just check all images from the data thats binded to the UITableView because that would be a big performance hit since it can be easily a hundred items. I am currently grabbing the items from a webservice that gives me the first 200 items.
How can I remove the cell altogther if the data==null example
NSUrl nsUrl = new NSUrl(cell.imageUrl);
NSData data = NSData.FromUrl(nsUrl);
if (data != null) {
InvokeOnMainThread (() => {
cell.imgImage = new UIImage (data);
//cell.imgImage.SizeToFit();
});
} else {
//I tried the below line but it does't work, I have a cell.index property that I set in the getCell method
_data.RemoveAt(cell.Index);//_data is the list of items that are binded to the uitableview
InvokeOnMainThread (() => {
//cell.Hidden = true;
});
}
You remove cells by removing them from your source and then you let UITableView know that the source has changed by calling ReloadData(). This will trigger the refresh process. UITableView will ask your source how many rows there are and because you removed one row from your data, the cell representing that row will be gone.
However ReloadData() will reload your entire table. There is a methods which allows you to remove cells specifically, named DeleteRows(). You can find an example here at Xamarin.
When deleting rows it is important that you update your model first, then update the UI.
Alternatively, I recommend you to check out MonoTouch.Dialog which allows interaction with UITableView in a more direct way. It also includes support for lazy loading images.

Resources