I have a problem with loading an image from an url to display in a table. I currently have the following code to handle the image loading in a class that extends UITableViewCell:
- (void) initWithData:(NSDictionary *) data{
NSDictionary *images = [data objectForKey:#"images"];
__block NSString *poster = [images objectForKey:#"poster"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSURL *posterURL = [[NSURL alloc] initWithString:poster];
NSData *imageData = [NSData dataWithContentsOfURL:posterURL];
if (imageData != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
// 4. Set image in cell
self.backgroundImage.image = [UIImage imageWithData:imageData];
[self setNeedsLayout];
});
}
});
self.backgroundImage.image = [UIImage imageNamed:#"static"];
}
The initWithData method is called from the ViewController in the tableView:cellForRowAtIndexPath: delegate. Everything works as expected until i scroll. From what i read, the TableView cells are recycled and because the images are being loaded async, i get rows with wrong images. Also, the images are not cached and are loaded again whenever the cell is displayed.
Eg: Scroll to the middle and immediately scroll back up. The first cell will have the image that's corresponding to the middle cell that didn't get to finish loading.
Any help or suggestions? Thank you very much :)
First of all as the comment mentioned, I would definitely recommend using an existing framework/component to do this job.
The best candidates are probably:
https://github.com/rs/SDWebImage
https://github.com/enormego/EGOImageLoading
OR if you also want a general networking library
https://github.com/AFNetworking/AFNetworking
That said, if you still want to try it on your own, you would probably want to implement caching with an NSMutableDictionary using the indexPath as the key, and the image as the value.
Assuming you have an initialized instance variable NSMutableDictionary *imageCache
In your cellForRowAtIndexPath method, before attempting to do any image loading, you would check to see if your cache already has an image for this index by doing something like this
if(! imageCache[indexPath])
{
// do your web loading here, then once complete you do
imageCache[indexPath] = // the new loaded image
}
else
{
self.backgroundImage.image = imageCache[indexPath];
}
Related
I would like to create a pdfReader with a collectionview. What I want is to have a collectionview with Thumbnails of the pdf to display. So I use this in the viewDidLoad (to avoid the fact that it will generate the thumbnail in the collectionview each time we go down or up). It is generated one time, and it is without lag :
Loading the thumbnail of the pdf in viewDidLoad:
- (void)viewDidLoad
...
coverPdf = [NSMutableArray new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {
// Load image on a non-ui-blocking thread
NSString *pdfPath = nil;
NSURL *pdfUrl = nil;
CGPDFDocumentRef pdfRef = nil;
NSMutableArray *arr = [NSMutableArray new];
for (id cover in filePathsArray)
{
pdfPath = [categoryPath stringByAppendingPathComponent:cover];
pdfUrl = [NSURL fileURLWithPath:pdfPath];
pdfRef = CGPDFDocumentCreateWithURL((CFURLRef)pdfUrl);
[arr addObject:[self imageFromPDFWithDocumentRef:pdfRef]];
NSLog(#"first process");
}
coverPdf = [NSMutableArray arrayWithArray:arr];
dispatch_sync(dispatch_get_main_queue(), ^(void) {
[pdfCollectionView reloadData];
});
});
...
}
Generating the thumbnail:
- (UIImage *)imageFromPDFWithDocumentRef:(CGPDFDocumentRef)documentRef
{
CGPDFPageRef pageRef = CGPDFDocumentGetPage(documentRef, 1);
CGRect pageRect = CGPDFPageGetBoxRect(pageRef, kCGPDFCropBox);
UIGraphicsBeginImageContext(pageRect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, CGRectGetMinX(pageRect),CGRectGetMaxY(pageRect));
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, -(pageRect.origin.x), -(pageRect.origin.y));
CGContextDrawPDFPage(context, pageRef);
UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return finalImage;
}
Using the thumbnail:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ListPdfCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectionViewCell" forIndexPath:indexPath];
cell.productLabel.text = [filePathsArray objectAtIndex:indexPath.row];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {
// Load image on a non-ui-blocking thread
dispatch_sync(dispatch_get_main_queue(), ^(void) {
// Assign image back on the main thread
if ([coverPdf count] > indexPath.row)
{
cell.pdfImage.image = [coverPdf objectAtIndex:indexPath.row];
}
});
});
return cell;
}
I have two problems with that method :
- The first one is that the thumbnail is taking too much time to appear. When it is loaded, it works well.
- The second problem, is that the memory is continually increasing and even if I close the viewcontroller, and I come in it, it seems that the memory is not released. If I close the viewcontroller and come in 9 or 10 times, if crash the app.
In conclusion, how can I create a collection view by loading the thumbnails of the pdfs in advance and how to avoid a crash with the memory increasing ?
Thanks in advance.
SOLUTION :
For the fact that it takes too much time to appear, I just replaced the DISPATCH_QUEUE_PRIORITY_BACKGROUND with the DISPATCH_QUEUE_PRIORITY_HIGH. It is much better.
For the memory leak, I used the CGPDFDocumentRelease() function just at the end of the loop like this, and all works like a charm :
for (id cover in filePathsArray)
{
pdfPath = [categoryPath stringByAppendingPathComponent:cover];
pdfUrl = [NSURL fileURLWithPath:pdfPath];
pdfRef = CGPDFDocumentCreateWithURL((CFURLRef)pdfUrl);
[arr addObject:[self imageFromPDFWithDocumentRef:pdfRef]];
CGPDFDocumentRelease(pdfRef);//Line added
NSLog(#"first process");
}
I have bad news for you. Memory leak is not your problem but iOS 10 memory management bug
There is a memory management bug in iOS 10.0.1 and 10.0.2 in the CGContextDrawPDFPage() function.
You can find details here http://www.openradar.me/28415289
Also you can find useful this discussion https://github.com/vfr/Reader/issues/166
In few words possible workaround is not to create
CGPDFPageRef pageRef = CGPDFDocumentGetPage(documentRef, 1);
each time but use one CGPDFPageRef for all of your pdf files. Then set it NULL when you not needs it anymore.
Creating thumbnails lag
And in terms of solution for creating thumbnails. I can only suggest not to do it in this VC at all but create thumbnail for each .pdf in the app at the moment when this .pdf added to the app (or on app launch in background service if all pdfs stored in the app bundle). Then you can save this previews as .jpg or .png files with names equal to pdf names or set relationships between pdf and preview in any database or other storage if you have it in your app.
Then just reuse this previews in your collectionView.
I am relatively new to IOS programming.
I have a NSObject "Places" which is given below.
#interface Places : NSObject
#property(strong) NSString *placeName;
#property(strong) UIImage *placeImage;
#end
I am listing the array of Places objects in UITableView. On tableView:cellForRowAtIndexPath: images are fetching asynchronously from web urls. On tableView:didSelectRowAtIndexPath:, I pass the respective 'Places' object to another ViewController (detailViewController). In that detailViewController, I uses below code to display the image in a UIImageView.
self.imageView.Image = self.myPlaces.placeImage
My problem is when I pass that object, the image needn't be fetched to placeImage. Is there anyway to update the self.imageView on successive completion of image fetching to placeImage in mainViewController.
My code is given below. It is called in tableView:cellForRowAtIndexPath: of mainViewController.
NSURL *url = [NSURL URLWithString:#"http://imageurl_goes_here"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSData *imageData = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
if(imageData != nil)
{
place.placeImage = [UIImage imageWithData: imageData];
cell.cellImageView.image = place.placeImage;
}
});
});
Well, there are some techniques to archive that:
You can notify your second view controller every time you received image. E.g. in your inner dispatch_async. But this requires main controller to know much about second one.
You can use Key-Value Observing technique to observe every place's image updating. In this case you might also want to use some handy library that will do sanity for you (like unsubscribing when object is deallocated, otherwise you will get crash) like that one or any other.
This post of Matt Thompson may be helpful on other types of communications between instances.
For some reason when I'm downloading my image using GCD, the image will randomly start flickering.
I'll reset the content settings in simulator and it'll work once, then it'll just start flickering again.
This is the code I am using. I've got the reloads in there because if I don't reload it, the image doesn't show until I tap on the cell.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {
NSURL *url = [NSURL URLWithString:self.entries.arrayimage];
NSData *imgData = [NSData dataWithContentsOfURL:url];
dispatch_sync(dispatch_get_main_queue(), ^(void) {
cell.imageView.image = nil;
UIImage *img = [UIImage imageWithData:imgData];
cell.imageView.image = img;
[self.tableView reloadData];
});
});
return cell;
[self.tableView reloadData];
That's because you are using dispatch_async(dispatch_get_global_queue to async load imageData from file. Using this style load image in cellForRow will make cell image should should previous image first. Then finish async load, will call dispatch_sync(dispatch_get_main_queue(), to load the image you want to. Therefore, whenever you reloadData or any other methods to call cellForRow, the cell image will flicker.
I know you want to load image without blocking main thread, but it's not a good way.
Check out apple sample code for Lazy Image loading. And also I checked your code and found that you always downloading image from URL. Instead of that its good to download and save image in caches and then load image from next time in cellForRowAtIndexPath method from local caches if available.
Is this code in cellForRowAtIndexPath:?
If that is the case, the problem here is that all the cells are infinitely reloading the table. You should not be calling reloadData in any of datasource methods that are triggered by reloadData.
What you have is basically an infinite loop of reloading. (reloadData triggers cellForRowAtIndexPath: which once again triggers reloadData).
My suggestion is to use an external component for this as aeskreis posted in his comment.
SDWebImage is probably the best one out there and will allow you to simplify all of the code you have there into simply:
NSURL *url = [NSURL URLWithString:self.entries.arrayimage];
[cell.imageView setImageWithURL:url];
Okay Guys I found out the problem. It was constantly reloading the data which caused te flickering. instead of [self.tableView reloadData] I replaced it with this method:
[cell setNeedsLayout];
I believe this method detects if anything has been changed, and then updates it (from my memory of a couple hours ago so it's probably not 100% accurate), but that fixed my problem.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSURL *url = [NSURL URLWithString:self.entries.arrayimage];
NSData *imgData = [NSData dataWithContentsOfURL:url];
dispatch_sync(dispatch_get_main_queue(), ^{
UIImage *img = [UIImage imageWithData:imgData];
cell.imageView.image = img;
//[self.tableView reloadData];
[cell setNeedsLayout];
});
});
return cell;
I'm loading images from the server in UItableViewCell.
Since Each Image takes 10MB size It cause memory problem.
App crashes Whenever I do scroll over the tableView
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
locationcellObject=[self.tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
NSDictionary *temp= [sortedArray objectAtIndex:indexPath.row];
locationcellObject.title.text=[temp objectForKey:#"locationtitle"];
locationcellObject.subtitle_Lbl.text=[temp objectForKey:#"category"];
NSString *trimmedtitle = [[temp objectForKey:#"locationtitle"]stringByReplacingOccurrencesOfString:#" " withString:#""];
NSString *name=[NSString stringWithFormat:#"images/%#.png",trimmedtitle];
NSString *imageName=[NSString stringWithFormat:#"http://my_URL_HERE/%#",name];
_tempData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageName]];
UIImage *display=[[UIImage alloc]initWithData:_tempData];
locationcellObject.locationPic_img_View.image=display;
locationcellObject.locationPic_img_View.contentMode=UIViewContentModeScaleAspectFit;
return locationcellObject;
}
Is there any Easy way to do it??
Download your images in background thread, download images with in the block. by this way two thread will be running in your app main thread and background thread. it will be reduces load on main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Call your function or whatever work that needs to be done
//Code in this part is run on a background thread
dispatch_async(dispatch_get_main_queue(), ^(void) {
//Stop your activity indicator or anything else with the GUI
//Code here is run on the main thread
});
});
And also add downloaded images in cache using NSCache,by which next time your images will be loaded from cache,
you can check this link here you can find how to add images in cache
Please refer THIS tutorial. It describes fetching/loading images in UItableView in efficient way.
1 ) you should do the downloading in the background
2 ) make a thumbnail of the image (image with smaller size)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^ {
_tempData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageName]];
UIImage *display=[[UIImage alloc]initWithData:_tempData];
//make a thumbnail of the image
UIImage *display;
CGSize destinationSize = ...;
UIGraphicsBeginImageContext(destinationSize);
[originalImage drawInRect:CGRectMake(0,0,destinationSize.width,destinationSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//
dispatch_async(dispatch_get_main_queue(), ^{
//put result to main thread
locationcellObject.locationPic_img_View.image= newImage;
});
});
I'm trying to load thumbnail images from a remote site onto a UITableView. I want to do this asynchronously, and I want to implement a poorman's cache for the thumbnail images. Here's my code snippet (I'll describe the problematic behavior below):
#property (nonatomic, strong) NSMutableDictionary *thumbnailsCache;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
// ...after obtaining the cell:
NSString *thumbnailCacheKey = [NSString stringWithFormat:#"cache%d", indexPath.row];
if (![[self.thumbnailsCache allKeys] containsObject:thumbnailCacheKey]) {
// thumbnail for this row is not found in cache, so get it from remote website
__block NSData *image = nil;
dispatch_queue_t imageQueue = dispatch_queue_create("queueForCellImage", NULL);
dispatch_async(imageQueue, ^{
NSString *thumbnailURL = myCustomFunctionGetThumbnailURL:indexPath.row;
image = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:thumbnailURL]];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:image];
});
});
dispatch_release(imageQueue);
[self.thumbnailsCache setObject:image forKey:thumbnailCacheKey];
} else {
// thumbnail is in cache
NSData *image = [self.thumbnailsCache objectForKey:thumbnailCacheKey];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:image];
});
}
So here are the problematic behaviors:
When the UITableView loads, thumbnails don't show up on the initial set of cells. Only when a cell moves off screen then moves back on does the thumbnail show up.
Cache isn't working at all. From what I can tell, it fails to save the thumbnail to cache altogether. That is, this line fails:
[self.thumbnailsCache setObject:image forKey:thumbnailCacheKey];
The GCD queue is getting created/released for each cell. Furthermore, the queue name is the same every time. Is this bad practice?
I'd appreciate you guys pointing out anything you see that is wrong, or even any general approach comments. Thanks.
Update:
RESOLVED: I added a call to reloadRowsAtIndexPaths and now the thumbnail images load on initial rows that display
RESOLVED: The reason it was failing is because it was adding the image object to the dictionary before the other thread completed setting that object. I created an instance method to add object to the property dictionary, so that I can call it from inside the block, ensuring it gets added after the image object is set.
You should definitively take a look at SDWebImage.
It's exactly what your looking for.
SDWebImage is also very fast and can use multicore CPU's.
1) The reason no initial image is showing is because the cell is rendered with image = nil, and so it intelligently hides the image view.
2) Did you try moving this line inside your block ?
[self.thumbnailsCache setObject:image forKey:thumbnailCacheKey];
3) This is just a way to differentiate the queues to debug, and get info from the console, if you app crashes then you can see the name of the queue. This shouldn't be a problem you having the same name for this, since it does the same operation. You wouldn't want to use this name in another table view if you have the same logic.