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.
Related
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
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.
When loading my UICollectionView cells I call a method which downloads an image async.
During the download however, my collection view is reloaded and so when the async image is downloaded it is set in two different cells.
I have also tried using an NSOperationQueue where in dealloc I call cancelAllOperations:, but this didn't work.
What is the best way to cancel this download and can someone provide some sample code?
Thanks.
I think the best practice is to launch requests lazily as you need the images, cache the results, and have no expectations about the state of the collection when the request completes.
Reloading the collection is not an invalidating event for a request for an image in a certain cell. Scrolling away is, but the user might scroll back. So make the request, and in the completion block for the request cache the result and reloadItemsAtIndexPaths: on the index path associated with the request.
My answer here, provides working code.
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.
I am trying to find out what my options are for this problems in terms of code design.
I have a table view, each cell owns an instance of a data model.
An asyncronous http request for json containing all text data for data model is made. This contains information about all cells. When it is retreived, all cells are created with their models.
However, this also includes a url to retreive an image from. Requests for an image for each cell are made asyncronously as they are being created / the table is being populated.
As the images come back, I need to update the UIImage views of the cells. What are my options with regard to doing this in a clean way?
Things I've considered:
pass along the UIImageView reference into completion block of image request method and update it when received. If model is later changed, view does not update itself :\
Subscribe the cell views to their models notifications about having changed. Feels very wrong, each cell would pick it up and have to check if it's their model that sent it?
you can use any of the following
SDWebImage
HJCache
Three20
and many more..