Asynchronous Fetch completed: Is cell still being displayed? - ios

During a recent interview, I was asked a scenario like #9 of these common interview questions regarding downloading images asynchronously into a table view cell. I understand the necessity for it to be called in cellForIndexPath and asynchronously but I was stumped as to how to check to see if the cell is still in view after the async call is complete (see the bullet #3 excerpt below). In other words, after an async call, how can I determine whether the table cell I was fetching data for is still in the view.
When the image has downloaded for a cell we need to check if that cell
is still in the view or whether it has been re-used by another piece
of data. If it’s been re-used then we should discard the image,
otherwise we need to switch back to the main thread to change the
image on the cell.

You should start downloading your image in the background with a callback mechanism that can decide if the image should still be displayed after it's been loaded.
One option would be to subclass UIImageView or UITableViewCell and store a reference to the NSURL of the image. Then, when your callback is called, you could check if the image view or the cell's cached URL is the one of the image you have, and decide to display it or not.
I wouldn't recommend on:
relying on a view's tag as it requires some sort of association table between a NSURL and an integer, which requires a manager object and is not helping reusability of your code
relying on the cell's indexPath as updates of the table or cells being reused for other index paths could occur while the network request happened
A more advanced options is described in Associated Objects, by NSHipster:
When extending the behavior of a built-in class, it may be necessary to keep track of additional state. This is the textbook use case for associated objects. For example, AFNetworking uses associated objects on its UIImageView category to store a request operation object, used to asynchronously fetch a remote image at a particular URL.

You can simply check whether the UITableViewCell is still in the view or not by using the following method of UITableView:
// return indexPaths that are visible
var indexPathsForVisibleRows: [IndexPath]?
Not to check whether to reload a specific row or not, you can do it by using the following method:
func downloadImageForCell(indexPath: IndexPath) {
// Asynchronous download method here
// After download is completed. Call the below in mainqueue
if let indexPaths:[IndexPath] = self.tableView.indexPathsForVisibleRows {
// the above line checks if indexPath is available
if indexPaths.contains(indexPath) {
self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
}
}
}
Please let me know if you have any problems in implementing this code

Related

AFNetworking: how to associate asynchronous JSON request with UITableViewCell?

I need to make some requests for information that will be used to fill the text field of a table view cell. however that cell may be reused before the request finishes. how can I associate an AFNetworking GET request with the uitableview cell in such a way as to be able to cancel it in prepareForReuse?
( if I were not using AFNetworking it would be a no-brainer.)
I'm not sure why AFNetworking makes a big difference to the question.
My solution is to:
Keep the url of the current request in the custom cell.
Have a weak reference to the cell in the completion block for the network call and also capture the requested url.
Clear the cell's url in preprareForReuse.
In the network completion block before updating the cell check that the cell's captured URL still matches the cell's URL if not don't update the cell (just cache the data or update the model object without refreshing the cell).
I would tend to let the network request complete in most cases but if you are using Data Session Tasks you could store the NSURLSessionDataTask in the cell and call cancel on it in prepareForReuse. There may be other options in AFNetworking that are not so easy to cancel.
If your're associating an AFHTTPRequestOperation with each cell in tableview, you could manage and cancel your GET request like this:
#property (nonatomic, strong) AFHTTPRequestOperation *operation;
- (void)prepareForReuse{
[super prepareForReuse];
[self.operation cancel];
}
AFHTTPRequestOperation is simply a subclass of NSOperation.
You can't. A UITableViewCell is very temporary. It doesn't refer to a specific line of your table; it will be reused for different lines. Here's what you do:
Have a cache for the data that you want to display in your rows.
When a cell is created, you try to fill it with data from the cache.
If that fails, then you start an asynchronous request.
When the asynchronous request succeeds, it deposits its data in the cache. Then it determines which row of the table contains the data that was just downloaded, and reloads the line.
Without you doing anything, the OS will ask you to create the cell again, and this time the data is in the cache. Note that the cell could be a totally different one, but displaying the same data.
Note that the row containing the data may be different from the row that contained the data when you started the request. For example, the user might have added or removed items in the table, or changed the sorting order.

How do I prevent cellForItemAtIndexPath from executing before I have my data ready?

I'm returning images as Base64 data from my server to my application but I'm not going to ask my server for the images until I have the users' location, which I'm only going to have after the user logs in to the application (which displays the UICollectionView).
My problem is that collectionView:cellForItemAtIndexPath: is being called before I have the data ready for the cells. Which ways could I prevent this method from being called until my NSURLConnection has received a full response from my server and I have the Base64 encoded image data?
(It may also be relevant that I'm using the UICollectionView as an NSURLConnectionDataDelegate and NSURLConnectionDelegate and I have a seperate class (RenderMetadata()) which will fetch the images.)
Edit: Thanks for all of your innovative answers! I've figured out a way to implement what I was after. I'd explain but I haven't shared my code so I don't think it's a good idea to even bring it up because that would just lead to confusion. Basically I'd added an Object to my view controller in my storyboard which referred to that class which I was using to fetch the Base64 encoded image strings from my server.
Set collection view delegates and reload the collection when you receive the response. Before that, keep them nilled. If delegate and dataSource of the collection view are nil it won't load data.
func handleDataLoaded()
{
// Your code that handles data.
...
// Load the collection with data
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.reloadData()
}
You need to ensure that the value returned by numberOfItemsInSection: accurately reflects the number of cells you want in your collectionview at all times - ie. If you haven't loaded any data it should return 0
You can add a isDataLoaded flag to the view controller class, initially set to false, and changed to true once all data has been loaded.
Then, in the numberOfSections method return 0 if that flag is not set:
func numberOfSections() {
return self.isDataLoaded ? 1 : 0
}
When all data is loaded, call the reloadData method to force a reload:
self.isDataLoaded = true
self.reloadData()
Be sure to execute that code in the main thread
You can set the number of tows to 0 as suggested by Paulw11,
OR Set the dataSource of your collectionView or tableView only when you actually want it to display the cells
OR call reloadData when you are ready
But you should made it in another way, your viewController should take care of updating the collectionView with the downloaded images, and you should reaload/update/add every single cell (show it without the image or with a placeholder if appropriate) once its own image is downloaded so the user will not have to wait every image to download before see something.

Is it possible to build an asynchronous image downloading mechanism for UITableViews with just NSURLSession? How?

I want to build an image downloader for my UITableView so images can be loaded asynchronously into the table view.
With NSURLSession automatically using NSURLCache to cache HTTP requests, will I be able to depend on that solely as my caching mechanism? I notice many use NSCache to save the downloaded image, but if NSURLSession already implements a cache, is that necessary? If anything, isn't it bad, as I'm duplicating a copy of the image in memory?
Furthermore, if it's possible, how do I use this in conjunction with UITableView? Cell reuse presents an interesting issue with assigning the result of a background transfer.
I can't simply download the image using NSURLSessionDownloadTask and in the completion block set the cell's image, as by the time it downloads the cell may have been reused and thus causing it to set it to the wrong cell.
let downloadTask = session.downloadTaskWithURL(URL, completionHandler: { location, response, error in
var dataFetchingError: NSError? = nil
let downloadedImage = UIImage(data: NSData.dataWithContentsOfURL(location, options: nil, error: &dataFetchingError))
dispatch_async(dispatch_get_main_queue()) {
// Might end up on the wrong cell
cell.thumbnail.image = downloadedImage
}
})
So how do I make sure that the assigned image makes its way to the correct cell, and that I'm caching these downloads properly?
I'm aware of SDWebImage and the like, but I'd like to try to solve this problem without the use of a library.
You can just have the final dispatch to the main queue see if the cell was still visible before updating the image cell. You can do this using cellForRowAtIndexPath (not to be confused with the similarly named UITableViewDataSource method):
dispatch_async(dispatch_get_main_queue()) {
if let updateCell = tableView.cellForRowAtIndexPath(indexPath) as? MyCell {
updateCell.thumbnail.image = downloadedImage
}
}
(Note, this assumes that the index path of the cell cannot possibly change while the asynchronous update is in progress. If it's possible that you can insert/delete rows while the update is in progress, you should not just use the old indexPath, but rather go back to the model and recalculate the appropriate index path for this cell.)
Obviously, those UIImageView categories offer other advantages besides just updating the image view asynchronously. For example, if the cell is reused, these categories will cancel any old pending network requests for that cell. This is important so that if you scroll quickly, the currently visible cells will otherwise get backlogged behind download requests for cells that may have long since scrolled out of view.
But if you're only concerned about the making sure that a cell is still visible before updating the image, the above should address that.
Regarding your cache question, there are two different types of cache to discuss: RAM cache and persistent storage cache. Both SDWebImage and AFNetworking UIImageView categories implemented their own RAM cache (using NSCache) for performance. For persistent storage caching, though, SDWebImage implemented their own, and AFNetworking argued that one should rely on the built-in NSURLCache for that. I'm sympathetic to the SDWebImage perspective because (a) historically NSURLConnection caching to persistent storage on iOS was inconsistent; (b) caching can easily be disturbed by the server using the wrong headers in the responses; and (c) Apple has been annoying opaque on the criteria for when something gets cached and when it doesn't.
So bottom line, there's probably a good argument to implement a NSCache mechanism if you want silky-smooth scrolling, but you might be able to get away with relying on NSURLCache for persistent storage caching.

What is a better way to deal with data after an async call using NSURLConnection?

This is the current logic that I'm using to populate a table view with NSURLConnection. It doesn't seem elegant to me.
Table View Controller's viewDidLoad method calls "sendConnection" method in my api wrapper class with the URL string as a parameter. This method makes the NSURLConnection. In connectionDidFinishLoading (which is in my wrapper class), another method is called (also in the wrapper class) with the connection as a parameter. This method extracts the URL from the connection object and examines it. It then uses a switch statement to deal with the data depending on the URL. The data is stored in variables in the wrapper class itself. By the time cellForRowAtIndexPath is called, the async call has finished and the data has been processed.
Is there a better way of doing this?
My reason for asking this is as follows:
I want to refresh a cell with a new height and a new text label when it is clicked. The data for this text label will be retrieved from the server upon the cell being tapped. Each cell will have slightly different data in the label (each cell represents a 'user' and the label will display how many mutual friends you have with the user). I want to store the data in the cell itself when the data is retrieved and then place it into the text label. This doesn't seem possible with my current way of making URL calls.
Any help with how to achieve this would be appreciated.
Here is some pseudo code for a pattern I like to use in these situations. Maybe it will help you as well.
- (void)viewDidLoad {
//1. put up some type of progressHud or spinner
//2. call your NSURL wrapper
//3. in the completion block of your wrapper, set your datasource variables
//example: #property (nonatomic,strong) NSArray *listOfData;
//4. create a custom setter for your datasource that calls tableview reload
//5. enable a refresh function; like "pull to refresh" or a bar button
//6. when pull to refresh is tapped or called, just repeat these steps
}
- (void)setListOfData:(NSArray*)listOfData {
_listOfData = listOfData;
if (_listOfData) {
[self.tableView reloadData];
}
}
As I read your question again, here are a couple more thoughts:
the pattern above will work for your initial load, to create the list of people or friends, etc.
If you plan on making another round trip after the cell is tapped, then you have to consider a number of issues. This is similar to a common problem with lazy loading images into tableview cells. There are issues like scrolling to consider - what if the cell is scrolled off the view before the data returns, for example, what if the cell has been reused, now the data is not tied to that cell any longer.
There are many async image libraries available on Github that would be good to look at to see how they solved those issues. Generally they are keeping track of the item in the cell and then checking if the cell is still in view and if so, they set the image.
You have a similar issue to solve. Tap the cell, get the new data, then update the cell. Resizing the cell will require you to reload it.
Look into [tableview reloadRowsAtIndexPaths:(NSArray*) with RowAnimation:(UITableViewRowAnimation)];
hope that helps
best wishes;
You should have a "Data Model" which represents the content (that is the cells) of your Table View.
Since you have "rows" in your table view, it makes sense this data model is a kind of array (possibly a NSArray) whose elements keep the data and state of the cell.
The data for each cell should not only have all the "data" properties rendered in your cell (e.g. the label) but also its state:
When a user tabs on a cell it will start an asynchronous task. This task may take a while to finish since it fetches data from a remote server. Think of several seconds, or even longer. You need to keep track of pending update tasks, since your implementation should prevent the user to update a cell again before the corresponding pending update task has been finished.
There are several techniques to accomplish this. One way is to have a property in your "Cell Data" class which reflects this state, for example:
#interface CellModel : NSObject
#property (atomic) BOOL hasPendingUpdate;
...
When the cell will be rendered, you retrieve the value of the property and render the cell appropriately.
When the update task finishes, it updates its cell model data.
This model update will eventually update your Table View. There are several techniques to accomplish this. You should take care about thread-safety here and the "synchronization" of your Data Model and the table view cells. For example ensure the value of the hasPendingUpdate only changes on the main thread - since otherwise your rendered cell may become out of sync with the data model (not to mention race conditions in case you modify and access the property on different threads without synchronization primitives).
While the cell waits for an update, it should visually indicate this state (using a spinner for example) and disable the action to start an update task.
Very much recommended is a "Cancel" button, which either cancels a certain cell update task or all pending update tasks.
When the user moves away from this view, you may consider to cancel all pending tasks.

NSURLConnection delay leads to an empty UItableViewCell without the picture

My app downloads the user's Facebook profile picture with an asynchronous (NSURLConnection) connection so that it can display the profile picture on a customized UITableViewCell. The problem is; the customized tableViewCell is created before the picture gets downloaded so the delay leads to an empty tableViewCell without the picture. How can I solve this problem?
My approach is to reach each cell by using a (for-in enumeration) and (a tag for each cell) in "connectionDidFinishLoading" method.
So guys, what do you think about my approach and do you have any better approaches???
Thanks for your help,
E.
Trying to tag and loop through your cells to find matches won't work because of UITableViewCell reuse. There are only as many cells in memory as you can see on screen, and these cells get recycled to display different data. Therefore, you won't be able to create a tag for each row in your table view, because the table view is only using a handful of cells.
What you should do instead is create a UITableViewCell subclass that knows how to asynchronously download and display in the image itself. Using the UIImageView category from AFNetworking is perfect for doing something like this, instead of having to manage the URL connection yourself. Import this category in your cell subclass, and write a method that calls setImageWithURL: to asynchronously download and display the image. Also, make sure to overload the UITableViewCell method prepareForReuse, in which you should call cancelImageRequestOperation on your image view. This is so the request to download the image is cancelled if the cell is reused before the download is complete.

Resources