I have a code to show image from remote URL.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = nil;
imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:image]];
if (imageData){
dispatch_async(dispatch_get_main_queue(), ^{
// UIKit, which includes UIImage warns about not being thread safe
// So we switch to main thread to instantiate image
UIImage *image1 = [UIImage imageWithData:imageData];
self.imageLabel.image = image1;
});
}
});
But it takes 2 seconds to download the image. I tried to put animated loading image to the UIImageView, but not worked. How can I implement a loading image for that 2 second?
Usually there is no built in method in iOS to provide a placeholder image. You can accomplish this task using a framework called SDWebImage.framework. In one of my projects I had to display placeholder images while the main images are loading from the server. I used the same framework and displayed images using UICollectionView, the code I used is:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CollectionCell *Cell = [collectionView
dequeueReusableCellWithReuseIdentifier:#"MyCell"
forIndexPath:indexPath];
[Cell.imageview setImageWithURL:url placeholderImage:[UIImage imageNamed:#"image.png"]];
return myCell;
}
Related
I am using lazy loading to show images on a table view.
But I need to create a tableview with multiple images in every cell.Which can be scrolled.
All images are loaded from server only
How can I create this without any lagging for table scrolling ?
Is there any tutorial available for this
Try this code. SDWebImage. It downloads image from server and save it to device cache.
Also if you don't want save it to cache then you might have a look at AFNetworking.
There is another option. Using GCD (Grand Central Dispatch).
Example Code :
// Get the filename to load.
NSString *imageFilename = [imageArray objectAtIndex:[indexPath row]];
NSString *imagePath = [imageFolder stringByAppendingPathComponent:imageFilename];
[[cell textLabel] setText:imageFilename];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
dispatch_sync(dispatch_get_main_queue(), ^{
[[cell imageView] setImage:image];
[cell setNeedsLayout];
});
});
Use the same for showing multiple images. Using this will increase the performance of loading tableview definitely.
Refer this to know more about GCD
Try this https://github.com/nicklockwood/AsyncImageView .Easy to download images asyncronously from server.
I need to resize a large locally stored image (contained in self.optionArray) and then show it in the collectionView. If I just show it, iOS trying to resize the images as I scroll quickly causing memory-related crashes.
In the code below, the collectionView will scroll smoothly, but sometimes if I scroll extremely fast, there will be an incorrect image that shows and then changes to the correct one as the scrolling decelerates. Why isn't setting the cell.cellImage.image to nil fixing this?
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CustomTabBarCell" forIndexPath:indexPath];
cell.cellImage.image = nil;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
cell.cellImage.image = nil;
UIImage *test = [self.optionArray objectAtIndex:indexPath.row];
UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
dispatch_sync(dispatch_get_main_queue(), ^{
cell.cellImage.image = localImage2
cell.cellTextLabel.text = #"";
[cell setNeedsLayout];
});
});
}
return cell;
}
- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize {
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
EDIT:
I added another async to cache first and nil and initialized the cell.image. I'm having the same issue on the initial fast scroll down. However, on the scroll back up, it's flawless now.
I added this:
-(void)createDictionary
{
for (UIImage *test in self.optionArray) {
UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
[localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:[self.optionArray indexOfObject:test]]];
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
if (!localImageDict) {
localImageDict = [[NSMutableDictionary alloc]initWithCapacity:self.optionArray.count];
}
else {
[localImageDict removeAllObjects];
}
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
[self createDictionary];
});
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CustomTabBarCell" forIndexPath:indexPath];
cell.cellImage.image = nil;
cell.cellImage.image = [[UIImage alloc]init];
if ([localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]]) {
cell.cellImage.image = [localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]];
cell.cellTextLabel.text = #"";
}
else {
cell.cellImage.image = nil;
cell.cellImage.image = [[UIImage alloc]init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *test = [self.optionArray objectAtIndex:indexPath.row];
UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
[localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:indexPath.row]];
dispatch_sync(dispatch_get_main_queue(), ^{
cell.cellImage.image = shownImage;
cell.cellTextLabel.text = #"";
[cell setNeedsLayout];
});
});
}
}
return cell;
Taking a closer look at your code sample, I can see the source of your memory problem. The most significant issue that jumps out is that you appear to be holding all of your images in an array. That takes an extraordinary amount of memory (and I infer from your need to resize the images that they must be large).
To reduce your app's footprint, you should not maintain an array of UIImage objects. Instead, just maintain an array of URLs or paths to your images and then only create the UIImage objects on the fly as they're needed by the UI (a process that is called lazy-loading). And once the image leaves the screen, you can release it (the UICollectionView, like the UITableView does a lot of this cleanup work for you as long as you don't maintain strong references to the images).
An app should generally only be maintaining UIImage objects for the images currently visible. You might cache these resized images (using NSCache, for example) for performance reasons, but caches will then be purged automatically when you run low in memory.
The good thing is that you're obviously already well versed in asynchronous processing. Anyway, the implementation might look like so:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CustomTabBarCell" forIndexPath:indexPath];
NSString *filename = [self.filenameArray objectAtIndex:indexPath.row]; // I always use indexPath.item, but if row works, that's great
UIImage *image = [self.thumbnailCache objectForKey:filename]; // you can key this on whatever you want, but the filename works
cell.cellImage.image = image; // this will load cached image if found, or `nil` it if not found
if (image == nil) // we only need to retrieve image if not found in our cache
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *test = [UIImage imageWithContentsOfFile:filename]; // load the image here, now that we know we need it
if (!test)
{
NSLog(#"%s: unable to load image", __FUNCTION__);
return;
}
UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)];
if (!localImage2)
{
NSLog(#"%s: unable to convert image", __FUNCTION__);
return;
}
[self.thumbnailCache setObject:localImage2 forKey:filename]; // save the image to the cache
dispatch_async(dispatch_get_main_queue(), ^{ // async is fine; no need to keep this background operation alive, waiting for the main queue to respond
// see if the cell for this indexPath is still onscreen; probably is, but just in case
CustomTabBarCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell)
{
updateCell.cellImage.image = localImage2
updateCell.cellTextLabel.text = #"";
[updateCell setNeedsLayout];
}
});
});
}
return cell;
}
This assumes that you define a class property of thumbnailCache which is a strong reference to a NSCache that you'll initialize in viewDidLoad, or wherever. Caching is a way to get the best of both worlds, load images in memory for optimal performance, but it will be released when you experience memory pressure.
Clearly, I'm blithely assuming "oh, just replace your array of images with an array of image filenames", and I know you'll probably have to go into a bunch of different portions of your code to make that work, but this is undoubtedly the source of your memory consumption. Clearly, you always could have other memory issues (retain cycles and the like), but there's nothing like that here in the snippet you posted.
I had a similar problem but went about it a different way.
I also had the issue of "pop-in" as images that were loaded async were flashed as they come in until finally the correct one was shown.
One reason this is happening is that the current indexpath for the cell that was initially dequeued did't match the index of the image you are putting into it.
Basically, if you scroll quickly from 0-19 and the cell you want to update is #20 and you want it to show image #20, but it's still loading images 3, 7, 14 asynchronously.
To prevent this, what I did was track two indices; #1) the most recent indexpath that reflects the actual position of the cell and #2) the index corresponding to the image that is actually being loaded async (in this case should actually be the indexpath you are passing into cellforitematindexpath, it gets retained as the async process works through the queue so will actually be "old" data for some of the image loading) .
One way to get the most recent indexpath may be to create a simple method that just returns an NSInteger for the current location of the cell. Store this as currentIndex.
I then put a couple if statements that checked that the two were equal before actually filling in the image.
so if (currentIndex == imageIndex) then load image.
if you put an NSLog(#"CURRENT...%d...IMAGE...%d", currentIndex, imageIndex) before those if statements you can see pretty clearly when the two do not match up and the async calls should exit.
Hope this helps.
I found the wording of what chuey101 said, confusing. I figured out a way and then realized that chuey101 meant the same.
If it is going to help anyone, images are flashed and changed because of the different threads that are running. So, when you spawn the thread for image operations, its going to get spawned for a specific cell no, say c1. But, at last when you actually load your image into the cell, its going to be the current cell that you are looking at, the one that you scrolled to - say c2. So, when you scrolled to c2, there were c2 threads that were spawned, one fore each cell, as you scrolled. From what I understand, all these threads are going to try loading their images into the current cell, c2. So, you have flashes of images.
To avoid this, you need to actually check that you are loading the image that you want into the cell that you mean to load to. So, get the collectionviewcell indexpath.row before loading image into it (loading_image_into_cell). Also, get the cell for which you spawned off your thread to before you spawn off the thread i.e. in the main thread (image_num_to_load). Now, before loading, check that these two numbers are equal.
Problem solved :)
I am making a simple reddit app for a school project. I am loading my
data from reddit via json (http://www.reddit.com/.json for example) with AFNetworking library.
I am displaying each reddit thread with a prototype cell, which
contains a UIImageView for the post thumbnails.
I am trying to use AFNetworking to lazy load the images, with the
setImageWithURLRequest method.
the problem: when the app launches all the thumbnails load lazily as they should as I scroll down the tableView. As soon as the cell is out of the view and I scroll back up to it, the thumbnail has been replaced with the placeholder image -- even though it loaded the correct thumbnail before scrolling past.
Relevant code from cellForRowAtIndexPath method. the lazy loading is being called in the setImageWithURLRequest block
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"threadCell";
SubredditThreadCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSDictionary *tempDictionary = [self.subredditArrayFromAFNetworking objectAtIndex:indexPath.row];
NSDictionary *singleThreadDict = [tempDictionary objectForKey:#"data"];
if ( [[singleThreadDict objectForKey:#"thumbnail"] isEqualToString:#"nsfw"] ){
cell.postImageThumbnail.image = [UIImage imageNamed:#"NSFWIcon"];
}
else if ([[singleThreadDict objectForKey:#"thumbnail"] isEqualToString:#"self"]){
cell.postImageThumbnail.image = [UIImage imageNamed:#"selfIcon"];
}
else if ([[singleThreadDict objectForKey:#"thumbnail"] length] == 0){
cell.postImageThumbnail.image = [UIImage imageNamed:#"genericIcon"];
}
else{
NSURL *thumbURL = [NSURL URLWithString:[singleThreadDict objectForKey:#"thumbnail"] ];
[cell.postImageThumbnail setImageWithURLRequest:[NSURLRequest requestWithURL:thumbURL]
placeholderImage:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
if (request) {
[UIView transitionWithView:cell.postImageThumbnail
duration:1.0f
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
[cell.postImageThumbnail setImage:image];
}
completion:NULL];
}
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error)
{
NSLog(#"failure loading thumbnail");
}
];
}
return cell;
}
Don't directly update the cell's postImageThumbnail when the image is done downloading.
Instead, tell the UITableView to refresh just that cell, using – reloadRowsAtIndexPaths:withRowAnimation:.
To get better performance, UITableView will re-use a cell-object once it's been scrolled offscreen, to show different data that is currently visible. (This is why dequeueReusableCellWithIdentifier: starts with "dequeueReusable", instead of "makeNew".) This lets UITableView only create about as many cells as are visible, instead of having to create and destroy as many cells as there are rows in the table. By the time your networking request succeeds, the cell that the success: block captures is being used to display another row, and you're over-writing that row's image.
I have a UITableViewCell that contains 4 photos and i get these photos from the web but the problem is when i scroll down the UITableView these photos are downloaded again
And this is the code:
ITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cellPhoto"];
if (cell == nil) {
NSArray *nibObject = [[NSBundle mainBundle] loadNibNamed:#"CustomCellThumbnails" owner:self options:nil];
cell = [nibObject objectAtIndex:0];
}
// Get the photos
int getPhotos = indexPath.row * 4;
for (int i = 1; i <= 4; i++) {
if (getPhotos < [imageArray count])
{
UIButton *imageButton = (UIButton*)[cell viewWithTag:i];
NSString *url = [imageArray objectAtIndex:getPhotos];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#"http://www.siteweb.com%#",url]]];
UIImage *imageFieldProfile = [[UIImage alloc] initWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
// Set the phoyo to the UIButton
[imageButton setBackgroundImage:imageFieldProfile forState:UIControlStateNormal];
[imageFieldProfile release];
// Set the corner of UIButton
[imageButton.layer setCornerRadius:5.0];
[imageButton.layer setMasksToBounds:YES];
imageButton.tag = getPhotos;
});
});
[imageButton addTarget:self action:#selector(displayPhoto:) forControlEvents:UIControlEventTouchUpInside];
}
getPhotos ++;
}
You should use the view controller to fill the cells, not the UITableViewCell. If you do that, it's not only a better coding style, it's also easier to save the data.
Anyway, if you really must: initialize the UITableViewCell with some kind of storage table, so that you can store the data you downloaded: rewrite initWithStyle:reuseIdentifier: to initWithStyle:reuseIdentifier:usingCacheTable:
But, again, the correct way to do this is to load the data in the view controller and let the UIView subclasses simply only show stuff.
You should save or cache the images in another object when they are downloaded, not in the table view cell. Perhaps using some sort of data source or model object from which the table view cell requests the images, instead of having the table view cell directly make any URL requests. Then the model object can cache images after downloading and before handing them to the cell.
You could use a combination of lazy loading image views in combination with local caching. This would be rather easy to accomplish using ASIHTTPRequest in combination with correctly setup caching flags.
ASIHTTPRequest is extremely easy to use and its caching is very well controllable.
In contrast to the other solutions suggested, I would stick to use UITableView and its UITableViewCells as this will allow you to use de/queued cells without having to build such logic yourself.
I have used such solution for a major newsmagazine app (over 2mio downloads) and am totally satisfied with the results.
I've got a custom UITableViewCell class whose model object performs an asynchronous download of an image which is to be displayed in the cell. I know I've got the outlets connected properly in IB for WidgetTVC, I know that image data is being properly returned from my server, and I've alloc/init'd the widget.logo UIImage too. Why is the image always blank then in my tableViewCell? Thanks in advance.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
Widget *theWidget = [widgetArray objectAtIndex:indexPath.row];
static NSString *CellIdentifier = #"WidgetCell";
WidgetTVC *cell = (WidgetTVC*)[self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:#"WidgetTVC" owner:self options:nil];
cell = self.widgetTVC;
self.widgetTVC = nil;
}
[cell configureWithWidget:theWidget];
return cell;
}
In my WidgetTVC class, I have the following method:
- (void)configureWithWidget:(Widget*)aWidget {
self.widget = aWidget;
self.nameLbl.text = aWidget.name;
[self.logoIvw setImage:aWidget.logo]; // logoIvw is the image view for the logo
}
Finally- I've got the callback method that sets the logo UIImage property on the Widget model object asynchronously (simplified):
(void)didReceiveImage:(ASIHTTPRequest *)request {
// I pass a ref to the Widget's logo UIImage in the userInfo dict
UIImage *anImage = (UIImage*)[request.userInfo objectForKey:#"image"];
UIImage *newImage = [UIImage imageWithData:[request responseData]];
anImage = newImage;
}
In didReceiveImage:, you're only modifying the local pointer anImage. You need to set the image property of your UIImageView in order to update what gets displayed. Instead of stashing a reference to your widget's UIImage, pass a reference to the UIImageView, and in didReceiveImage: do something like
UIImageView *imageView = (UIImageView*)[request.userInfo objectForKey:#"imageView"];
UIImage *newImage = [UIImage imageWithData:[request responseData]];
imageView.image = newImage;
Perhaps the best solution would be to have your model object have the image as a property and the display object subscribe to the image property's changes through KVO and update itself whenever the image property changes.
OK- there were a couple things wrong here. Thanks to bosmacs and Ed Marty as both of your comments were used to get to the solution.
First, I added a method to the Widget object to get the logo asynchronously:
- (void)asyncImageLoad {
...
// logo is a UIImage
[AsyncImageFetch fetchImage:&logo fromURL:url];
}
And my own AsyncImageFetch class looks like this:
+ (void)fetchImage:(UIImage**)anImagePtr fromURL:(NSURL*)aUrl {
ASIHTTPRequest *imageRequest = [ASIHTTPRequest requestWithURL:aUrl];
imageRequest.userInfo = [NSDictionary dictionaryWithObject:[NSValue valueWithPointer:anImagePtr] forKey:#"imagePtr"];
imageRequest.delegate = self;
[imageRequest setDidFinishSelector:#selector(didReceiveImage:)];
[imageRequest setDidFailSelector:#selector(didNotReceiveImage:)];
[imageRequest startAsynchronous];
}
+ (void)didReceiveImage:(ASIHTTPRequest *)request {
UIImage **anImagePtr = [(NSValue*)[request.userInfo objectForKey:#"imagePtr"] pointerValue];
UIImage *newImage = [[UIImage imageWithData:[request responseData]] retain];
*anImagePtr = newImage;
}
Finally, per Ed, I added this to the configureWithWidget method that helps set up my WidgetTVC:
[aCoupon addObserver:self forKeyPath:#"logo" options:0 context:nil];
And when a change is observed, I update the imageView and call [self setNeedsDisplay]. Works like a charm. Any way I can give you both points?