NSURLConnection delay leads to an empty UItableViewCell without the picture - ios

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.

Related

Avoid UITableViewCell updating content when scrolled

I've found some similar questions already on SO, but nothing that seems to address this specific problem.
I'm using a UITableView with around 25 dynamic cells. Each cells contains a hidden UIProgressView. When the user taps on the download button within the cell to download that item, the UIProgressView is displayed and it indicates the progress of the download.
I've achieved this by assigning a tag to each cell which is equivalent to its corresponding downloadItemID. Each instance of MyCell has its own progressBar property.
This works fine as long as the table view is not scrolled during the download. It works also when the user is downloading multiple items at the same time. The code looks like this:
UIProgressView *theProgressBar;
for (MyCell *cell in self.tableView.visibleCells)
{
if (cell.tag == downloadItemID) theProgressBar = cell.progressBar;
}
float progressPercentage = (float)completedResources / expectedResources;
[theProgressBar setProgress:progressPercentage animated:YES];
The problem is that when the user scrolls the table view, the cell and progress view are transferred to another cell. It's simple enough to reset and hide the progress view for the new cell, but when the original/downloading cell is scrolled back into view, no progress is visible.
I've tried caching active progress bars into a dictionary and reallocating them back to the original cell in cellForRowAtIndexPath, but this is giving me the same result: no visible progress after scrolling the cell off and on the screen. Even if I can get them to show up, I'm doubtful I can get this to work seamlessly by rolling my own caching method.
What I need is to keep cells in memory. But can I work around dequeueReusableCellWithIdentifier? This whole problem has arisen because I had to switch to a dynamic system of allocating the cells, but it is too dynamic. Can I create the cells dynamically, but keep them in memory all the time once created, or at least keep the ones that are currently downloading?
(I understand the reasons why cell reuse is the preferred way to go with table views.)
You are working against the framework. As vadian says in the comment, you need to separate the logic and state information from the cells and put them elsewhere.
Here is one way of doing it.
Create a class to hold each download state, like a download ongoing flag, download progress, title, URL, etc.
In your view controller, create and add all your download objects to an array when the view controller is created. The first object corresponds to the first row in the table.
Whenever you dequeue a cell, populate it with data from the array. The NSIndexPath row property serves as the index into the array.
All your updates (download progress) updates the download objects in the array, then update the cell content using the updated object. You can use UITableView cellForRowAtIndexPath to get the cell for a specific array index, if you get nil there is no need to update it.

Downloading images only for visible UITableViewCells

In UITableViewController I have custom cells with thumbnail image which is cached by me using TMCache. So the basic workflow of loading cells is:
Fill cell lables with data from model
Check if I have a thumbnail image cached
If yes, than get it from cache...
If not, download it from web...
And me concerns are that when I don't have anything in cache I'd start downloading a lot of images (even if I have set maximum number of concurrent tasks) so when the user scrolls for example a 100 rows my tasks array in AFHTTPSessionManager will be dealing with all of then even if user is not interested in many of them.
So I came with this solution:
When usere scrolls down and downloading begins, but in a moment this cell gets off the screen, I want to cancel NSURLSessionDataTaskfor this cell. But... I don't know how to check which cell should cancel its task and the more important issue, what if task is completed in 90% and I cancel it (waste of data transfer)? I've noticed that in Facebook app they're not cancelling those tasks because when you scroll up they are loaded.
I wonder if this is a good approach or maybe I'm trying to overcomplicate everything?
Check out the UITableViewDelegate Protocol Reference.
Specifically, you can use the following methods to track cells' appearance and disappearance:
– tableView:willDisplayCell:forRowAtIndexPath:
- tableView:didEndDisplayingCell:forRowAtIndexPath:
Why waste bandwidth? Do this:
in UITableViewControllerDataSource::cellForRowAtIndexPath: start a timer to go off in, say 200 ms or something, which will begin the download process when it's triggered. Associate the timer with the indexPath (or cell).
In UITableViewControllerdelegate::tableView:didEndDisplayingCell:: kill the timer if it hasn't already gone off.
There is a method of UITableViewCell that you can override: prepareForReuse. It will get called when table view is no longer need this cell and reusing memory for another cell that is becoming visible.
I think it will be a good start for you to cancel request associated with this cell.

UITableViewCell image from web

I'm going to download images and titles from web (probably using AFNetworking) and insert inside UITableViewCell. Since images are not same size I'll need to calculate size for every cell.
Currently in my Post model I have title and imageURL. Should I first download all images, insert in array and then add to cell or should I use that AFNetworking function inside cellForRowAtIndexPath and download images from there?
Since I'm going to need images in both cellForRowAtIndexPath and heightForRow (for calculation), downloading all images first and storing in array might be better solution but dunno if cache is doing all the work so I could go with second approach?
Thanks.
Fot the better performance,
You should approach for the Small sizes of images or few.
downloading all images first and storing in array.
IF the images are huge, then should go for the Asynchronous task to display.
and then Use the custom UIImageView within the cell to show the image.

On iOS, how to avoid image re-fetching in a UITableView with a fixed or variable number of rows?

If we fetch a small image (say, 60 x 60) from the Internet, inside of the method:
-(UITableViewCell *) tableView:(UITableView *) tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath { ... }
What if there are a fixed 25 number of rows vs if the length can be variable (say, 100, or can be 1000)? If we use dequeueReusableCellWithIdentifier to "reuse" a cell, and we probably will need to remove the subviews in this cell (one of which is the 60 x 60 image), and so when the user scroll up and down the list, the cells are reused, and images are re-fetched from the net, and it can be pausing here and there while scrolling.
But if I remove the dequeueReusableCellWithIdentifier and always allocate a new cell:
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:TableViewCellIdentifier];
then when I scroll down and up again, the NSLog shows that the images are still being fetched. Is there a way to prevent that, if we don't want to use an array separately to store the fetched images?
(right now, the behavior is, initially, about 7 images are fetched, and if the table is scrolled down 3 rows, then 3 images are fetched, and if the table is scrolled back up 3 rows, then 3 images are fetched again)
you should have an image caching mechanism. download the image if it does not exist. Set an expiration where you will need to refetch it. Then display it.
Next time you load it. you display the already downloaded image and check the expiration. If its expired you download it again, and update the display.
pretty simple.
Only complication is, after the download should the image be over written. I test this with a GUID. When the image download is requested I put a UUID in the tableview or image object itself. I also send that UUID along with the download task. when the download returns you test the download's UUID against the display item's UUID. if they are Equal. then you update the display. if they have changed, then another download was requested and overrode yours. Therefore the images was changed by that process and a download will be finishing soon that does have that UUID and will change the image with its downloaded contents.
Hope that helps :)
You could use image caching as Volure mentioned. I also suggest you implement some form of lazy loading for those images. There are many different implementations for this. This project is a subclass of UIImageView that supports asynchronous loading.
Also, take a look at this question, the answers could be helpful.
hope this helps.
As far as the scroll view pausing your should load your images (if they are not cached or need updating as per DarkAngel's answer) on another thread. I'd use NSOperationQueue.
Here's a good tutorial on setting up NSOperationQueue. A good idea is to use a shared queue in your app delegate that you can just add tasks to when you need it.
Just use SDWebImage or AFNetworking. They both offer a category of UIImageView giving you the caching mechanism.
What that means is if the image downloader library (SDWebImage or AFNetworking) has found an image in its cache, then it will just get that one from disk rather than redownload the image from the web.
You're seeing that lag is most likely because in your cellForRowAtIndexPath: table view delegate method, you're probably downloading the thumbnail image using NSURLConnection on the main thread.
SDWebImage or AFNetworking will essentially do all the hard work for you and make your table responsive. All that you have to do is really:
[myThumbnailImageView imageWithURL:imageURL placeholder:[UIImage imageNamed:#"myPlaceholderImage.png"]];
Once you got that line in there, that's it. Your real image will appear once its downloaded, until then you can continue to smoothly scroll the table view. Each cell will display a placeholder image that you specified (see above line of code) when the image has not been downloaded.
For More convenience to download image use EGOImageLoader/EGOImageView Download this class by this link https://github.com/enormego/EGOImageLoading.
From this EGOCache is used to store your image. First time you download, while scrolling the UITableview get the image from EGOCache store. In that class it store the Downloaded image with key. The key is hashed value of your imageUrl. Use same key to retrieve your image from cache. Tamilarasan

Update a cell in a filtered table from background?

I have a table that displays images. It can be searched live, which adjusts the cells being displayed. The images are loaded in the background using a concurrent NSOperation subclass over the network when the image is not already immediately available.
After the image has been fetched, I want to update the table to display it.
How can I update the right cell?
I cannot rely on the cell that I have built in tableView:cellForRowAtIndexPath:, because the cell may have been repurposed for another row.
I cannot rely on the indexPath passed into tableView:cellForRowAtIndexPath:, because it may have changed since the update began due to a search term change.
I cannot search the visible cells in the table, because the cells themselves are instances of UITableViewCell and have no metadata.
I cannot search my filtered list, because I don't know what table to apply the changes to.
What am I missing?
I do this exact operation in my app in the app store. I created a notification handler in my view controller that gets notified when an image arrives. That image has tag associated with it - something I can map to the view that needs it.
When a cell is created, I add a UIImageView to the contentViews array, with a unique tag that matches the tag associated with the image (and you can offset these with like 10,000 to avoid conflicts).
Then I ask the UITableView for the array of visible cells. I iterate through them, asking each cell's contentView for a viewWithTag:tag (the unique tag). If found, I have the right UIImageView, and I set the image.
Obviously when providing cells to the tableView, you have to always provide the proper tag to the ImageView.
The simplest solution is probably just to subclass UITableViewCell to add a tracking property. Something like this:
#interface TrackingTableViewCell : UITableViewCell
#property (strong) id trackingValue;
#end;
#implementation TrackingTableViewCell
// Default #synthesize as of Xcode 4.4!
#end
When you set up the cell, set the trackingValue to the NSURL you are downloading the URL from. When you finish loading the image, check the trackingValue of the visible cells. If you find one that matches, great. If not, don't set it anywhere.
You could theoretically do the same thing with the objc_setAssociatedObject() API, but I don't really see the value in doing that over just creating a simple subclass to track the value for you.

Resources