Downloading images only for visible UITableViewCells - ios

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.

Related

Prevent Single Row from Refreshing in UITableView

I have a custom section header in my UITableView that contains A UICollectionView of some avatar images. The data is sourced once and cached for the images (images change so infrequently that it doesn't warrant real time updates).
I was hoping to make it so this header never redraws again even if the UITableView refresh is called. The reasoning for this is every time you take action, it causes the images to flicker as they're being redrawn from their default anonymous silhouette to the actual image of the person. The images are cached, but it doesn't matter because
I assume this is not possible by design - a UITableView will destroy everything and reload it all over again every time the refresh is called, correct?
I just wish I could hook into the refresh and preserve the section header, and reload the rows only.
Thanks for any ideas/guidance, I know this is a little uncommon but I don't want to move the header out into its own view because i'm using a UITableViewController directly, and it would be a real pain to have to embed it in a containerview and all that.
Yes, even you cached the image and set by local or something, it will take time to load. If you are using lazy loading, it will be splash ( for sure :) ). So, the only possible thing is, you have to update your tableView after refreshing is using tableView.beginUpdates and tableView.endUpdates. Input some add cell or remove cell base on the data after refreshing

Animate progress on 100s of UIButtons without blocking main thread

I have a UITableView with at most a few hundred cells (not all visible at once). Each cell contains a UIButton with a way to indicate progress of an upload. A URLSession performs the uploads in background tasks.
Currently, the session delegate is the UIViewController that manages the cells. As a result, the session calls delegate
.URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend: to periodically inform the delegate of the progress of sending content to the server.
In the delegate method, I find the UIButton associated with this task and animate the new progress (I can find the button because I make button.identifier = task.identifier).
This approach forces me to find the button every time the delegate method is called. This seems indirect and I am wondering if there is a better way to do this — there could be 100s of buttons so worried about runtime.
I was thinking to make the button be the session delegate, but that goes against MVC and the button reference may disappear or change in a table view causing undefined behavior (though it sort of makes sense to only update buttons that are actually in memory).
there could be 100s of buttons
No, there couldn't. Cells that do not appear on the screen do not exist at all (because cells are reused in a table view). So you only need to worry about the cells that are actually visible at any one moment. See UITableView visibleCells and indexPathsForVisibleRows. Thus, even though your approach is not extremely efficient, it isn't extremely inefficient either.
However, the correct way to do this is to use the progress object vended by your upload task. When the upload starts, tell the cell or the button or whatever to start observing the progress object's fractionCompleted using key-value observing. Now the cell or the button or whatever is in direct contact with that one task and can update itself every time it hears that the fractionCompleted has changed. When the cell stops being displayed, stop observing. There's a little more to it (i.e. to cope with reused cells that scroll onto the screen when the corresponding task is already in progress) but that's the basic architecture you want.

How to add cell to tableview's reuse queue before 'cellForRow:'?

Creating a cell costs a lot of time and make the first scroll lagging, so I want to create a cell and add it to tableview's reuse queue before cellForRow: called.
I use dequeueReusableCellWithIdentifier: in viewDidLoad, but when I scroll the table, the cell is being created again.
In general all drawing methods of scrollview should be kept as simple as possible to avoid lag. This means you should prepare your data/model in viewDidLoad/viewWillAppear or even in previous ViewController. Your cellForRow should be as simple as set this image(s) and those text(s) - no checks, no expensive operations such as bluring, retrieving data from CoreData/Network, etc.
If you are not sure which thing exactly causes your lag, you should learn how to use TimeProfiler. If you feel lost in documentation, have a look this(quite outdated though) tutorial.
With thus said I cannot be able to help you anymore until you post some code which we could discuss.

Disable cell reuse on UICollectionView

I am using a UICollectionView in my app with gesture recognizers on the individual cells which allow the user to "slide open" the cell to reveal more data underneath.
The problem is, I am reloading the data in the CollectionView very often; as the app receives updates once every 3 seconds or so. This results in unwanted behavior with the collectionview cells being reused while a cell is in the process of being slid.
The user will start to slide a cell, the app will receive an update, reloadData, and a different cell will start receiving the gesture instead, and begin sliding.
I have tried disabling the app's updates while the slide is occurring, but that caused other complications within the app, so I am wondering if there is a way to disable the cell reuse, (I will only have 20 cells max, so I don't think there would be a large drop in performance).
Thank you!
Why don't you use a flag like needsReload and set it, if new data is available. After a slide you check for that flag and reload the collectionView, if needed? Is this not working?
If you don't want cell reusing, just use a default scrollView and put all your views in it!?
Disabling reuse is simple. Just don't use the dequeueReusableCell method.
Instead just alloc, init your cells. I would be careful of the performance and memory implications of doing so though...

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

Resources