Lazy Loading UICollectionView Images - objective-c

There's a UIImageView for every cell of the UICollectionView and I need to load them so that as you scroll through the collection they show up properly and every image loads on its cell at its index path, once.
Every image is downloaded from Kinvey.
Code to download every image:
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, cell.bounds.size.width, cell.bounds.size.width)];
[cell addSubview:imageView];
[KCSFileStore downloadData:#"id for item at index path"] completionBlock:^(NSArray *downloadedResources, NSError *error) {
if (error == nil) {
KCSFile* file = downloadedResources[0];
dispatch_async(dispatch_get_main_queue(), ^(void){
imageView.image = [UIImage imageWithData:file.data];
});
NSLog(#"Downloaded.");
} else {
NSLog(#"Error: %#", error.localizedDescription);
}
} progressBlock:nil];
That "dispatch_async(dispatch_get_main_queue(), ^(void)..." is something I tried to solve my problem downloading images asynchronously, but that didn't work.
Thanks in advance.

why you try to download the images.
If you want to perform lazy loading for images in collectionView using third party library. then SDWebimages
is the best third party library for that.this library not only perform the lazy loading but provide multiple other option also. just take a look.
Hope this will Help You and solve your problem.

Related

App Crashes when too many images are in TableView

I have a table that I am populating with information stored in Runner Objects. Each Runner object has a property called fileName that corresponds to an image saved in the document directory.
If I start with an empty table, I can add runners and their photos just fine until I reach about 8 runners. When I try to add a photo for the 8th or so runner, the app crashes. It seems to be a memory issue, but I don't see how this can be when all I'm storing locally are references to images in the directory.
I pass a runner object to my set up cell method in my RunnerCell class and set up the cell like so:
if (currentRunner.fileName != nil) {
if ([manager fileExistsAtPath:fullPath]){
UIImage *originalImage = [[UIImage alloc] initWithContentsOfFile:fullPath];
if ([currentRunner.photoOrientation isEqual: #"portrait"]) {
CGImageRef imageRef = [originalImage CGImage];
UIImage *rotatedImage = [UIImage imageWithCGImage:imageRef scale:1.0 orientation:UIImageOrientationRight];
self.runnerImageView.image = rotatedImage;
} else {
self.runnerImageView.image = originalImage;
}
self.addPhotoLabel.hidden = true;
}
}
As per documentation states that you mustn't load image to a UITableViewCell on main thread. That is why your app is crashing.
Because when you load image on main thread it became too much laggy when you scroll because dequereusable cell reassign cell and again load image for upcoming indexpath's cell and until the image loads it block the main thread which cause laggy behaviour of tableview.
Please load your cached image in a background thread. use this code.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {
if ([currentRunner.photoOrientation isEqual: #"portrait"]) {
CGImageRef imageRef = [originalImage CGImage];
UIImage *rotatedImage = [UIImage imageWithCGImage:imageRef scale:1.0 orientation:UIImageOrientationRight];
dispatch_sync(dispatch_get_main_queue(), ^(void) {
self.runnerImageView.image = rotatedImage;
});
} else {
dispatch_sync(dispatch_get_main_queue(), ^(void) {
self.runnerImageView.image = originalImage;
});
}
});

Poor UICollectionView Scrolling Performance With UIImage

I have a UICollectionView in my app, and each cell is a UIImageView and some text labels. The problem is that when I have the UIImageViews displaying their images, the scrolling performance is terrible. It's nowhere near as smooth as the scrolling experience of a UITableView or even the same UICollectionView without the UIImageView.
I found this question from a few months ago, and it seems like an answer was found, but it's written in RubyMotion, and I don't understand that. I tried to see how to convert it to Xcode, but since I have never used NSCache either, it's a little hard to. The poster there also pointed to here about implementing something in addition to their solution, but I'm not sure where to put that code either. Possibly because I don't understand the code from the first question.
Would someone be able to help translate this into Xcode?
def viewDidLoad
...
#images_cache = NSCache.alloc.init
#image_loading_queue = NSOperationQueue.alloc.init
#image_loading_queue.maxConcurrentOperationCount = 3
...
end
def collectionView(collection_view, cellForItemAtIndexPath: index_path)
cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
image_path = #image_paths[index_path.row]
if cached_image = #images_cache.objectForKey(image_path)
cell.image = cached_image
else
#operation = NSBlockOperation.blockOperationWithBlock lambda {
#image = UIImage.imageWithContentsOfFile(image_path)
Dispatch::Queue.main.async do
return unless collectionView.indexPathsForVisibleItems.containsObject(index_path)
#images_cache.setObject(#image, forKey: image_path)
cell = collectionView.cellForItemAtIndexPath(index_path)
cell.image = #image
end
}
#image_loading_queue.addOperation(#operation)
end
end
Here is the code from the second question that the asker of the first question said solved the problem:
UIImage *productImage = [[UIImage alloc] initWithContentsOfFile:path];
CGSize imageSize = productImage.size;
UIGraphicsBeginImageContext(imageSize);
[productImage drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
productImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Again, I'm not sure how/where to implement that.
Many thanks.
Here's the pattern I follow. Always load asynch and cache the result. Make no assumption about the state of the view when the asynch load finishes. I have a class that simplifies the loads as follows:
//
// ImageRequest.h
// This class keeps track of in-flight instances, creating only one NSURLConnection for
// multiple matching requests (requests with matching URLs). It also uses NSCache to cache
// retrieved images. Set the cache count limit with the macro in this file.
#define kIMAGE_REQUEST_CACHE_LIMIT 100
typedef void (^CompletionBlock) (UIImage *, NSError *);
#interface ImageRequest : NSMutableURLRequest
- (UIImage *)cachedResult;
- (void)startWithCompletion:(CompletionBlock)completion;
#end
//
// ImageRequest.m
#import "ImageRequest.h"
NSMutableDictionary *_inflight;
NSCache *_imageCache;
#implementation ImageRequest
- (NSMutableDictionary *)inflight {
if (!_inflight) {
_inflight = [NSMutableDictionary dictionary];
}
return _inflight;
}
- (NSCache *)imageCache {
if (!_imageCache) {
_imageCache = [[NSCache alloc] init];
_imageCache.countLimit = kIMAGE_REQUEST_CACHE_LIMIT;
}
return _imageCache;
}
- (UIImage *)cachedResult {
return [self.imageCache objectForKey:self];
}
- (void)startWithCompletion:(CompletionBlock)completion {
UIImage *image = [self cachedResult];
if (image) return completion(image, nil);
NSMutableArray *inflightCompletionBlocks = [self.inflight objectForKey:self];
if (inflightCompletionBlocks) {
// a matching request is in flight, keep the completion block to run when we're finished
[inflightCompletionBlocks addObject:completion];
} else {
[self.inflight setObject:[NSMutableArray arrayWithObject:completion] forKey:self];
[NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
// build an image, cache the result and run completion blocks for this request
UIImage *image = [UIImage imageWithData:data];
[self.imageCache setObject:image forKey:self];
id value = [self.inflight objectForKey:self];
[self.inflight removeObjectForKey:self];
for (CompletionBlock block in (NSMutableArray *)value) {
block(image, nil);
}
} else {
[self.inflight removeObjectForKey:self];
completion(nil, error);
}
}];
}
}
#end
Now the cell (collection or table) update is fairly simple:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
NSURL *url = [NSURL URLWithString:#"http:// some url from your model"];
// note that this can be a web url or file url
ImageRequest *request = [[ImageRequest alloc] initWithURL:url];
UIImage *image = [request cachedResult];
if (image) {
UIImageView *imageView = (UIImageView *)[cell viewWithTag:127];
imageView.image = image;
} else {
[request startWithCompletion:^(UIImage *image, NSError *error) {
if (image && [[collectionView indexPathsForVisibleItems] containsObject:indexPath]) {
[collectionView reloadItemsAtIndexPaths:#[indexPath]];
}
}];
}
return cell;
}
In general bad scrolling behaviour for UICollectionViews or UITableViews happens because the cells are dequeued and constructed in the main thread by iOS. There is little freedom to precache cells or construct them in a background thread, instead they are dequeued and constructed as you scroll blocking the UI. (Personally I find this bad design by Apple all though it does simplify matters because you don't have to be aware about potential threading issues. I think they should have given a hook though to provide a custom implementation for a UICollectionViewCell/UITableViewCell pool which can handle dequeuing/reusing of cells.)
The most important causes for performance decrease are indeed related to image data and (in decreasing order of magnitude) are in my experience:
Synchronous calls to download image data: always do this asynchronously and call [UIImageView setImage:] with the constructed image when ready in the main thread
Synchronous calls to construct images from data on the local file system, or from other serialized data: do this asynchronously as well. (e.g. [UIImage imageWithContentsOfFile:], [UIImage imageWithData:], etc).
Calls to [UIImage imageNamed:]: the first time this image is loaded it is served from the file system. You may want to precache images (just by loading [UIImage imageNamed:] before the cell is actually constructed such that they can be served from memory immediately instead.
Calling [UIImageView setImage:] is not the fastest method either, but can often not be avoided unless you use static images. For static images it is sometimes faster to used different image views which you set to hidden or not depending on whether they should be displayed instead of changing the image on the same image view.
First time a cell is dequeued it is either loaded from a Nib or constructed with alloc-init and some initial layout or properties are set (probably also images if you used them). This causes bad scrolling behaviour the first time a cell is used.
Because I am very picky about smooth scrolling (even if it's only the first time a cell is used) I constructed a whole framework to precache cells by subclassing UINib (this is basically the only hook you get into the dequeuing process used by iOS). But that may be beyond your needs.
I had issues about UICollectionView scrolling.
What worked (almost) like a charm for me: I populated the cells with png thumbnails 90x90. I say almost because the first complete scroll is not so smooth, but never crashed anymore.
In my case, the cell size is 90x90.
I had many original png sizes before, and it was very choppy when png original size was greater than ~1000x1000 (many crashes on first scroll).
So, I select 90x90 (or the like) on the UICollectionView and display the original png's (no matter the size). hope it may help others.

how load many photos from url in background (asynchronous)

i have this method i use to load many images to scroll view, but when the images load from the url my scroll view is stuck and i can't understand how to load them in the background so the user will not feel it.
this method will call few times (8) in "for" cycle.
- (void)loadPhotosToLeftscroll{
//getting image information from JSON
NSMutableDictionary *photoDict;
photoDict = [leftPhotoArray lastObject];
//getting the photo
NSString *photoPath = [photoDict objectForKey:#"photos_path"];
NSLog(#"photo Path:%#",photoPath);
NSData * imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:photoPath]];
UIImage *image = [UIImage imageWithData:imageData];
// use the image how you like, say, as your button background
//calculating the hight of next photo
UIImageView *leftImage = [leftBlockScroll.subviews lastObject];
//allocating photoView
UIImageView *photoView = [[UIImageView alloc]initWithFrame:CGRectMake(5 , leftImage.frame.origin.y + leftImage.frame.size.height+5, image.size.width/2, image.size.height/2 )];
photoView.userInteractionEnabled=YES;
[photoView.layer setMasksToBounds:YES];
[photoView.layer setCornerRadius:3];
//getting items list
NSDictionary *sh_items = [photoDict objectForKey:#"items"];
//adding image button
UIButton *imageOverButton = [UIButton buttonWithType:UIButtonTypeCustom];
imageOverButton.frame = CGRectMake(0, 0, photoView.frame.size.width, photoView.frame.size.height);
[imageOverButton addTarget:self action:#selector(LeftimagePressed:) forControlEvents:UIControlEventTouchUpInside];
[imageOverButton setTag:[leftPhotoArray count]-1];
[photoView addSubview:imageOverButton];
//adding sh button to imageView
[self addThe_sh_signs:sh_items To_ImageView:photoView];
//subViewing the image to the scrollView
[self insert_Image:image toImageView:photoView in_Scroll:leftBlockScroll];
//calclulating the position of next imageView in scroll.
nextLeftPhotoHight = photoView.frame.size.height + photoView.frame.origin.y + 5;
//calculating the hight of the highest scroll view in both.
leftBlockScroll.contentSize = CGSizeMake(160, [self theSizeOfScrollViewHight]);
rightBlocScroll.contentSize = CGSizeMake(160, [self theSizeOfScrollViewHight]);
isLoadindContant = NO;
[self.view reloadInputViews];
[leftBlockScroll reloadInputViews];
}
please do not send me to some link that trying to explain how to use the asynchronous.
Try to explain according the method you see here.
Im here for any question, that you need to ask to help me.
You will have to do it asynchronously in a proper way. I do not think there is any way around that. I subclassed an UIImageView object and placed many instances of it within the cells of a talbe (in your case within the scroll view). The subclass objects are initialized with an url and load their image asynchronously (with some caching so that the image is not loaded every time).
This tutorial helped me much in the beginning:
http://www.markj.net/iphone-asynchronous-table-image/
You will just have to adopt that to your scroll view. The underlaying principle remains the same.

What is the most efficient way to ensure content in cell are not re-used incorrectly

I am trying to add youtube videos to my UITableView via the following code. The trouble I had was the fact that the cell was being reused and the UIWebviews were also being reused causing the wrong videos to appear in the wrong cells.
I tried to resolve this issue by explicitly removing the UIWebview from superview before I set the video and also if the cell does not contain a youtube video.
But this seems rather expensive and inefficient. Can anyone advise me on a more efficient way of doing this so that the webview (containing the video) does not get reused in the wrong cells?
Code snippet from "cellforRowAtIndexPath":
//Re-create a UIWebview for youtube video everytime the cell is loaded
UIWebView *thisYouTubeVideo = [[UIWebView alloc]init];
//Check if this cell contains a youtube video
if (self.youtube_url !=nil)
{
//This is the first time I explicitly remove the UIWebview from cell's contentview
for (UIView *subview in [self.contentView subviews]) {
if ([subview isKindOfClass:[UIWebView class]]) {
[subview removeFromSuperview];
}
}
//This is just to set the frame of the UIWebview
thisYouTubeVideo.frame = CGRectMake(CELL_TEXT_LEFT_MARGIN, currentYAxisValue, 240, 240);
//Key for caching
NSString *videokey=[NSString stringWithFormat:#"%#",self.answerForCell.youtube_url];
//Check if webview has been created before, just load it from the dictionary (cache)
if([self.youtubeVideoCache objectForKey:videokey]) {
thisYouTubeVideo = [[self.youtubeVideoCache objectForKey:videokey]retain];
}
else
{
//If webview does not exist in cache, load the url to webview and store in cache
NSString * videoHTML = [self embedYouTube:self.answerForCell.youtube_url frame:CGRectMake(CELL_TEXT_LEFT_MARGIN, currentYAxisValue, 240, 240)];
[thisYouTubeVideo loadHTMLString:videoHTML baseURL:nil];
[self.youtubeVideoCache setObject:thisYouTubeVideo forKey:videokey]; //Save webview in dictionary
}
//add the uiwebview to the cell's content view
[self.contentView addSubview:thisYouTubeVideo];
[thisYouTubeVideo release];
}
else
{
//If the cell DOES not contain a youtube video, do the following
thisYouTubeVideo.frame = CGRectZero;
//This is the second time I am explicitly removing the webview from cell's content view
for (UIView *subview in [self.contentView subviews]) {
if ([subview isKindOfClass:[UIWebView class]]) {
[subview removeFromSuperview];
}
}
}
Use your videoKey as the cell identifier
[tableView dequeueReusableCellWithIdentifier:videoKey],
but keep in mind that this will impact on memory usage, because you do not reuse the allocated cells any more.

UITableViewCell Image not updating

I can update the detailTextLabel.text and the UITableViewCell shows the changes at runtime, but if I try to update the imageView.image it does not change the visible image. Any idea as to why? I have tried calling a refresh on the UITableViewCell specifically but to no avail.
-(void)getImageForURL:(NSURL*)url row:(UITableViewCell*)cell {
UIImage*image;
image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url];
cell.imageView.image = image; // Does not work..
cell.detailTextLabel.text = #"test"; // Works
}
Try calling [cell setNeedsLayout] after setting the image, if it's the first image you're setting for the cell.
Make sure the cell style is UITableViewCellStyleDefault, because other cell types may always return nil imageView, instead of creating one on demand.
Check whether the image is nil or not. If its nil then the Image is not fetched from the URL correctly.
I too was having problems with adding the image to my imageView that was constrained in a tableview cell and was not updating with cell.setNeedsLayout(). Calling update methods on the tableview after the image was added did the trick for me:
cell.setNeedsLayout()
if #available(iOS 11.0, *) {
tableView.performBatchUpdates(nil, completion: nil)
}
else {
tableView.beginUpdates()
tableView.endUpdates()
}