iOS CollectionView: Display View, then download images, then display images - ios

I have a collection view, which contains cells in each section, and each cell has an image in it.
Possibly, the collection view holds 20 or more cells.
I want to load the images off the internet into the cells. The issue is that this can take a few seconds, and I want the collectionView to be displayed even if the images have not downloaded completely.
The collection view is given an array that contains the URLs, and so far, I have been downloading off the internet within collection view cellforitematindexpath
However, the view only becomes visible after all the cells have been loaded, and since each call to collection view cellforitematindexpath downloads an image, if 20 images are pulled off of URLs, it takes way to long.
What can I do to display the view, and THEN download the images, and then display them?
Hope I made myself understandable, if not, please ask!
Thanks!
C

Yes, you could use SDWebImage (as mention at comment above).
Another interesting side you could face out, that if image haven't load yet and user scroll view, it could be problems with dequeueReusableCellWithReuseIdentifier:forIndexPath, so it's better to download images throw id <SDWebImageOperation>.
Prepare for reuse will be something like this:
- (void)prepareForReuse
{
[super prepareForReuse];
if (self.imageOperation)
[self.imageOperation cancel];
self.imageOperation = nil;
}
And load will be like this:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
self.imageOperation = [manager downloadWithURL:[NSURL URLWithString:urlString]
options:SDWebImageRetryFailed
progress:nil
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
if (image)
{
self.imageView.image = image;
}
}];

Related

Dynamically modifying the height of cells in a UICollectionViewCell in Objective C

I need to make a Pinterest style newsfeed. I have created a collection view with 2 columns. I am downloading the photos async in cellForItemAtIndexPath:
- (UICollectionViewCell *)collectionView:(UICollectionView*)collectionView cellForItemAtIndexPath:(NSIndexPath*)indexPath {
TFNewsObject *obj = [self.sortedDataSource objectAtIndex:indexPath.row];
[imageView sd_setImageWithURL:url placeholderImage:nil options:SDWebImageRefreshCached
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
obj.tfImage = image;
}];
return cell;
}
The problem is that I need to set the size of the cell in heightForItemAtIndexPath and there the image has not been downloaded yet because it did not enter cellForItemAtIndexPath yet. I am setting a fixed size of 100 for the height.
I need to somehow trigger heightForItemAtIndexPath after I've downloaded the image in cellforItemAtIndexPath.
Another thing that I've tried is downloading the image in heightForItemAtIndexPath synchronous and calculating the height after the image downloaded. In that manner it displays correctly with each cell with a different height depending on the image, but it takes forever to load all the images.
Is there a way to modify the cell's height after I've downloaded the image? Thanks!
Try this
[imageView sd_setImageWithURL:url placeholderImage:nil options:SDWebImageRefreshCached
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
obj.tfImage = image;
collectionView .reloadItems(at: [indexPath])
}];
In -setImageWithURLRequest:..., when the image loads it in a hash table so you should then call -reloadItemsAtIndexPaths: with the current image path. This will reload the cell and thus cause the CollectionView to check what size the cell should be.
Another solution that you can try is to invalidate the collection view layout when the image is downloaded.
- (UICollectionViewCell *)collectionView:(UICollectionView*)collectionView cellForItemAtIndexPath:(NSIndexPath*)indexPath {
TFNewsObject *obj = [self.sortedDataSource objectAtIndex:indexPath.row];
[imageView sd_setImageWithURL:url placeholderImage:nil options:SDWebImageRefreshCached
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
[self.collectionView.collectionViewLayout invalidateLayout];
}];
return cell;
}
After this return a appropriate size in the delegate method:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return /* size */;
}
I got into similar problem while i developed something similar to pinterest layout.
Even we had a lag while downloading images asynchronously and once the download is done refreshing cell by cell will blow away the user and will lead to UI jerks.
The solution which i could suggest is,
For example when the post is created with the image, Calculate the size (width and height) of the image and save it in the DB. So the size of the image is available before the image is downloaded or cached and we could scale the cell to the height of the image and display a default image and when the actual image is cached or downloaded it would silently replace the default image without any UI jerks.
If you are using services like cloudinary then you can avoid the pain of storing the image size (height and width). The cloudinary api will give you the height and width of the image.

Which image loading method should i prefer?

I have app with TableView and cells. In each cell there is UIImageView.
All images are stored on server.
I can use two different methods to load images. Which of them should prefer and why?
Method A :
Use library like SDWebImage to load image and place it in cellForRowAtIndexPath function. So image will be downloaded when cell is created.
Method B :
When i load JSON with image list from server i can create array of UIImages. In each of them i will asynchronously download image from server. And in cellForRowAtIndexPath function i can just assign one of previously created UIImages to current cell image.
Method A - SDWebimage is best for you.
and solve reuse in tableviewcell check this link : Handling download of image using SDWebImage while reusing UITableViewCell
Don't ever use method 2 to handle Images in your application. This is a data and memory wise expensive method. To the best of my understanding, this method would extensively increase the memory pressure. If you create an array of images that would remain in the memory as long as your view controller stays. As the size of this array increases the situation will get worse.
SDWebImage is far far better approach for this task. It saves the images to local storage once downloaded thus creating a cache of images. So you do not have to download the images again and again.
i wish to use AFNetworking if you don't need to cache image , this tools faster than SDWebImage when load image from server . //////
if you use custom cell => replace UITableViewCell with your name file for cell
-(void)fetchImageFromURL:(NSString*)imageURLString Cell:(UITableViewCell*)cell {
/*
_ This Function will accept the string url for Image
_ Also the cell that have image icon
_ After take paramter will make request to fetch image
_
1- if return image => will show Image
*/
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"%#", imageURLString]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
__weak UITableViewCell *weakCell = cell;
// get image with request
[[cell imageView ] setImageWithURLRequest:request placeholderImage:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
[[weakCell ImageIcon ] setImage:image];
[weakCell setNeedsLayout];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"couldn't load image with url :%#", url);
}];
}
The best choice in my opinion is to rely on a stable, fresh, followed and secure framework.
Recently come to the scene a great framework written in Swift called Nuke (Swift 3 - Xcode 8) . I think you could try Nuke, his power is the "preheat images" (preheating/prefetching means loading images ahead of time in anticipation of its use.), it's full compatible with Alamofire , already knowed by community (raywenderlich.com) and now is at v.4.x (stable and mature). This library have custom handlers and requests with many options.

Loading images of "Next" TableView Cell Asynchronously

So I am using SDWebImage to load images into the cells.
I want to preload images of the next few cells so that when the user scrolls, the image is already loaded.
Whats the ideal way to do this.
Current code -->
[cell.BGImage sd_setImageWithURL:[NSURL URLWithString:section.imageURL] placeholderImage:[UIImage imageNamed:#"PlaceholderF"]];
Ensuring that the current displayed image is kept at priority loading.
Any suggestions?
You can use SDWebImageDownloader method to download image before your cell load or in other controller. So it will cache image and when your cell load it will show immediately.
[[SDWebImageDownloader sharedDownloader]downloadImageWithURL:nil options:SDWebImageDownloaderContinueInBackground progress:^(NSInteger receivedSize, NSInteger expectedSize) {
} completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
}];
and you can set download image priority in LIFO manner using below code.
[[SDWebImageManager sharedManager].imageDownloader setExecutionOrder:SDWebImageDownloaderLIFOExecutionOrder];
Maybe this will help you.

Loading images asynchronously in UITableView with Autolayout

I have a UITableView which loads images asynchronously in a UITableView. As all the images are different heights, I set a height constraint according to the height of the image.
This works fine when I use DataWithContentsOfUrl, however freezes the UI. When I load asynchronously, the images pile up on top of each other like so:
http://i.stack.imgur.com/TEUwK.png
The code i'm using is as follows:
NSURL * imageURL = [NSURL URLWithString:img];
NSURLRequest* request = [NSURLRequest requestWithURL:imageURL];
cell.imageViewHeightConstraint.constant=0;
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * response, NSData * data, NSError * error) {
if (!error){
UIImage *imgz = [UIImage imageWithData:data];
[cell.CellImg setBackgroundImage:imgz forState:UIControlStateNormal];
[cell.CellImg sizeToFit];
cell.imageViewHeightConstraint.constant=result.width*(imgz.size.height/imgz.size.width);
[cell setNeedsUpdateConstraints];
}
}];
[cell updateConstraints];
When I scroll up and down a few times the images correctly position themselves.
The question doesn't specify the desired behavior for varying image sizes. Should the cell get taller to fit, or just the image view (what looks like a button from the code) within the cell?
But we should put aside that problem for a moment and work on the more serious problem with the code: it issues an unguarded network request from cellForRowAtIndexPath. As a result, (a) a user scrolling back and forth will generate many many redundant requests, and (b) a user scrolling a long way quickly will generate a request that's fulfilled when the cell that started it is gone - reused as the cell for another row.
To address (a), the datasource should cache fetched images, and only request those that haven't been received. To address (b), the completion block shouldn't refer directly to the cell.
A simple cache would look like this:
#property(strong,nonatomic) NSMutableDictionary *images;
// initialize this when you initialize your model
self.images = [#{} mutableCopy];
// move the network code into its own method for clarity
- (void)imageWithPath:(NSString *)path completion:(void (^)(UIImage *, NSError *))completion {
if (self.images[indexPath]) {
return completion(self.images[indexPath], nil);
}
NSURL *imageURL = [NSURL URLWithString:path];
NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error){
UIImage *image = [UIImage imageWithData:data];
self.images[indexPath] = image;
completion(image, nil);
} else {
completion(nil, error);
}
}];
}
Now, we fix the multiple request problem by checking first for the image in the cache in cellForRowAtIndexPath.
UIImage *image = self.images[indexPath];
if (image) {
[cell.CellImg setBackgroundImage:image forState:UIControlStateNormal];
} else {
// this is a good place for a placeholder image if you want one
[cell.CellImg setBackgroundImage:nil forState:UIControlStateNormal];
// presuming that 'img' is a string from your mode
[self imageWithPath:img completion:^(UIImage *image, NSError *error) {
// the image is ready, but don't assign it to the cell's subview
// just reload here, so we get the right cell for the indexPath
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}];
}
Also notice what isn't done in the completion block... we're fixing the cell reuse by not referring to the cell. Instead, knowing that the image is now cached, we reload the indexPath.
Back to image sizing: most apps like to see the table view cell get taller or shorter along with the variable height subview. If that's the case, then you should not place a height constraint on that subview at all. Instead, constrain it's top and bottom edges to the cell's content view (or include it in a chain of subviews who constrain to each other top and bottom and contain the topmost and bottommost subviews top and bottom edge to the cell). Then (in iOS 5+, I think), this will allow your cell to change hight with that subview constraint chain...
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = // your best guess at the average height here
Try the following along with constraint constant:
cell.imageViewHeightConstraint.constant=result.width*(imgz.size.height/imgz.size.width);
[table reloadRowsAtIndexPaths:#[indexPathForCurrentCell] withRowAnimation:UITableViewRowAnimationNone];
You can also set the animation.
Although reloadRowsAtIndexPaths works, it ends up causing the interface to be slow and laggy, especially when the user is scrolling. My solution was once the data was loaded to iterate through https://graph.facebook.com/v2.1/%#/?access_token=%#&fields=attachments for the object id, and then store the image height dimensions in an array. Then, in cellForRowAtIndexPath simply setting cell.imageViewHeightConstraint.constant to the value in the array. That way all the cells heights are set as soon as the cell loads, and the image fits into place perfectly as soon as it loads.

how to combine drawRect: and ImageCache to speed up table view scrolling

I need to implement my custom drawRect: method in my custom table cell in order to speed up tableview scrolling, however, there's an image the app should download from web, so I want to add image cache to my app. The origin implementation of my app is using SDWebImage library, which implement an imageView which offer download image method. If I want to add image in drawRect:, how to implement downloading image from web and cache them?
You need to use SDWebImage to download and cache the image. Then in delegate/completion block of that download operation, you need to assign the image to your a property and call setNeedsDisplay. setNeedsDisplay will cause your view to be redrawn. This should be called on mainQueue.
[[SDWebImageManager sharedManager] downloadWithURL:[NSURL URLWithString:theArticle.imageURL] options:SDWebImageLowPriority progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
dispatch_async(bakground_queue, ^{
//Here you probably want to dispatch_async to a background queue to do all the image resizing first before drawing on main queue
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.thumbnailImage = image;
[weakSelf setNeedsDisplay];
});
});
}];
In your drawRect, you need to draw from that property. For e.g
[self.image drawInRect:rect]
This is just basic principle of how this can be achieved. Probably needs more work to really optimize it (e.g cancel the downloading operation when not needed)
Edited:
SDWebImageManager has a delegate method to resize/transform image before storing it to disk cache. You might want to use that method instead of dispatch_async to background queue like above.

Resources