I know it's pretty common question. I've seen number of people ask here this type of a question, but I still can't seem to get it right.
I have a UICollectionView with pictures. I'm the getting the images from facebook so I'm using SDWebImage to load them.
Until here everything works ok.
The user can upload pictures that he took. so until the uploading process is finished and image was uploaded to facebook, I want the local image to be shown but with a layer (different alpha, activity etc') and once i get a notification from the server, the new image from facebook is replacing the local one.
My problem is that after adding the local images to the collectioView with the layer, more cells are displaying this layer and from time to time when i scroll up , the new images changes.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
PictureCell* cell = (PictureCell *)[_collectionView dequeueReusableCellWithReuseIdentifier:[PictureCell reuseIdentifier] forIndexPath:indexPath];
Picture *picture=(Picture *)[_campaign.pictures objectAtIndex:indexPath.row];
//If the picture is being uploaded, add the image from the local image property and set the blur layer
if (!picture.facebookPicture) {
cell.pictureImageView.image = picture.image;
[cell TaggleBlur:YES];
}
else
{
[cell.pictureImageView sd_setImageWithURL: [NSURL URLWithString:picture.facebookPicture.source ]placeholderImage:[UIImage imageNamed:#"actionBar_pico_icon"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
NSArray *visibleIndexPaths = [_collectionView indexPathsForVisibleItems];
if (error==nil) {
if ([visibleIndexPaths containsObject:indexPath]) {
cell.pictureImageView.image = image;
[cell TaggleBlur:NO];
}
}
}];
}
return cell;
}
Related
i'm pretty new in obj/Xcode, and i need to do a UIcollectionView how take images from url request.
I've try to catch the scroll event with scrollViewDidScroll, then call the my request function with the parameter page + 1 or -1; depending of the scroll.
But everytime i'm going on infinite loop, and my cell images donc stop loading, i think its because my way is wrong :
Catching the scroll action :
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
{
[self reload];
}
The function reload send the request and set the imgs.
How can i load properly the pages, and not 1000times for every scrolling.
Sorry for my english, and thank you
You dont need to call reload like that. As you scroll, 'cellForItemAtIndexPath' is automatically called. If theres a previously used cell in the table (out of view) then the cell is re-used for the new position it is populating - hence 'dequeueReusableCellWithReuseIdentifier' (hope you're using that?!).
So what you need to do is have code on that method itself, that will automatically 'lazy load' the image you want. Heres a copy of a cellForItemOfIndexPath method that achieves just that - the image is populated when its downloaded.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
VideoSearchCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
YoutubeSearchResult *cellSearchResult = [self.ytSearchResults objectAtIndex:indexPath.row];
if (cellSearchResult.thumbnailImage.image == nil)
{
//NSLog(#"loading image");
__weak VideoSearchCell *weakCell = cell;
__weak YoutubeSearchResult *weakResult = cellSearchResult;
NSURL *url = [NSURL URLWithString:cellSearchResult.thumbUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
UIImage *placeholderImage = [UIImage imageNamed:#"your_placeholder"];
[cellSearchResult.thumbnailImage setImageWithURLRequest:request placeholderImage:placeholderImage success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
weakResult.thumbnailImage.image = image;
weakCell.imageView.image = image;
[weakCell setNeedsLayout];
} failure:nil];
} else {
if (cell.imageView.image != cellSearchResult.thumbnailImage.image)
{
cell.imageView.image = cellSearchResult.thumbnailImage.image;
}
}
return cell;
}
So to explain - VideoSearchCell is my custom cell type. Its fairly simple, it just has a UIImageView on it called 'imageView'. Ive also got an array called 'ytSearchResults' - this contains an array of objects and some contain images, and some dont. As the images download, the results also get populated to that array so I dont download them more than necessary.
The code checks to see if theres an image in that array. If not, it does the downloading code.
By the way, I use __weak references to properties, because of ARC and not wanting to accidentally retain the cell, or the search result. Not enough time to explain all that here though! Hope the code helps.
One more thing - setImageWithURLRequest is a method from the very handy and extremely popular library AFNetworking (2.0). Get hold of that as you will use it all the time.
I do not recommend to load images before cell is actually used, because you never know if user will see that cell. You can start image loading when
- collectionView:cellForItemAtIndexPath: is called, and before starting download request you must check if download of this image is in progress, otherwise you will load it more than once.
I'm loading images asynchronously in my table view. On slow connections, the images in the cell keep flickering/changing (I assume it's from reusing the cells). I've followed the advice on some of the other related questions including:
Caching the image
Setting a placeholder image
It works fine on normal connection, but on EDGE and slow networks, the problem still occurs. I wonder if it is the global queue that is getting backed up with network requests? If so, how do I solve this?
Here is my code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// get the data for this photo post
NSDictionary *data = [self.streamPhotosData objectAtIndex:indexPath.row];
static NSString *CellIdentifier = #"StreamPhotoTableViewCell";
__block StreamPhotoTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"StreamPhotoTableViewCell"];
if (cell == nil) {
cell = [[StreamPhotoTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.delegate = self;
// check if image in cache, else cache it
if ([[[GlobalCaches getCurrentInstance] photoCache] objectForKey:[data objectForKey:#"photoGuid"]] != nil) {
// cache hit
cell.photoImageView.image = [[[GlobalCaches getCurrentInstance] photoCache] objectForKey:[data objectForKey:#"photoGuid"]];
} else {
// set a placeholder image
cell.photoImageView.image = [UIImage imageNamed:#"placeholder.png"];
// download the image asynchronously
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
UIImage *downloadedImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[data objectForKey:#"url"]]]];
if (downloadedImage) {
dispatch_async(dispatch_get_main_queue(), ^{
cell.photoImageView.image = downloadedImage;
// update cache
[[[GlobalCaches getCurrentInstance] photoCache] setObject:downloadedImage forKey:[data objectForKey:#"photoGuid"]];
});
}
});
}
return cell;
}
I had a similar issue once.
Why does it happen?
Let's say you're in cellForRow for row number 1, this image is not in the cache, so you're downloading it. Meantime, you scroll the tableView, and let's say that now you're in cellForRow for row number 8, which is the same cell instance.
Let's say that now you have this image in cache, so you're just setting the image with no time offset, and after the cell is with the right image, the image from row number 1 is ready, and remember that it is the same cell instance, thus the image is getting replaced.
Of course this is only one example.
You can try to 'remember' for which row you started to download the image, set a 'beforeTag' before you start the download, and after is, check if it is the same indexPath.row
NSInteger beforeTag = indexPath.row;
// download the image asynchronously
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
UIImage *downloadedImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[data objectForKey:#"url"]]]];
if (downloadedImage) {
dispatch_async(dispatch_get_main_queue(), ^{
if (indexPath.row == beforeTag) {
cell.photoImageView.image = downloadedImage;
}
// update cache
[[[GlobalCaches getCurrentInstance] photoCache] setObject:downloadedImage forKey:[data objectForKey:#"photoGuid"]];
});
}
});
Also, I would put this line in prepareForReuse in your custom cell:
cell.photoImageView.image = [UIImage imageNamed:#"placeholder.png"];
This method is called every time a UITableViewCell is getting reused, it is called just after you 'dequeueReusableCellWithIdentifier', if the cell is already exist. And it's okay to always put a placeHolder image, if the image is in cache, you wont notice this placeholder.
Hope it helps
Everything works fine except that the images in the cell are showing up after a disturbing delay. About 0,3 seconds after the tableView has finished loading. I think it's pretty ugly to show a blank image for a short time...
How can I make the tableView to show up just after the images from parse are loaded (why not from the cache first?). Or I could maybe make the launch screen hold on for a longer time..? The TableView is the initial view in the app.
queryForTable
- (PFQuery *)queryForTable {
PFQuery *query = [PFQuery queryWithClassName:self.parseClassName];
// If no objects are loaded in memory, we look to the cache
// first to fill the table and then subsequently do a query
// against the network.
if ([self.objects count] == 0) {
query.cachePolicy = kPFCachePolicyCacheThenNetwork;
}
[query orderByAscending:#"name"];
return query;
}
cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)objects
{
static NSString *simpleTableIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleTableIdentifier];
}
//loading icons
PFFile *thumbnail = [objects objectForKey:self.imageKey];
PFImageView *thumbnailImageView = (PFImageView*)[cell viewWithTag:100];
thumbnailImageView.file = thumbnail;
thumbnailImageView.image = [UIImage imageNamed:#"placeholder.jpg"];
[thumbnailImageView loadInBackground:^(UIImage *image, NSError *error) {
if (!error) {
}
}];
//cell
cell.textLabel.text = [objects objectForKey:self.textKey];
return cell;
}
Thanks in advance :)
There is no harm in loading the images after a short delay. This is a standard procedure and is followed by many world class apps (ever try scrolling in Facebook or Twitter app). The work around to this would be quite messy in logical and programmatic terms.
I think the best way to do this would be the way facebook does it on their app. Facebook shows an empty gray container where the image belongs and once the image has been downloaded they fade it in, in animation fashion. The reason I think this is the way to go is because it doesn't seem to make sense to load an image in and then potentially load another image when ready, better to just hold an empty container and load the image in once ready.
I have tableviewcontroller and data fecthed from the server. I use following class to download the data asyn. but my problem is data is loading when user sees the tableViewcontroller. I want data being loaded before user sees.
#import <SDWebImage/UIImageView+WebCache.h>
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
cell.textLabel.text = [[tableData objectAtIndex:indexPath.row] valueForKey:#"name"];
cell.textLabel.font = [UIFont fontWithName:#"BebasNeue" size:24];
cell.textLabel.textColor = [UIColor whiteColor];
NSString *imageURLString=[[tableData objectAtIndex:indexPath.row] valueForKey:#"logo"];
NSString* imageURL = [[tableData objectAtIndex:indexPath.row] valueForKey:#"picture"];
[cell.imageView setImageWithURL:[NSURL URLWithString:imageURL]];
}
Solution
With SDWebImage you can download the image first.
Where?
It depends on your implementation. Maybe in a previous controller, or in your appDelegate. If the tableview appears in your initial viewcontroller, chances are to absolutely depend on the network speed.
Example Code
Take a look at this code (extracted from the Readme of the library, check the source!):
Manager *manager = [SDWebImageManager sharedManager];
[manager downloadWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize)
{
// YOU DON'T NEED THIS
}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished)
{
if (image)
{
// MAYBE YOU WANT TO DO SOMETHING HERE
}
}];
Then, in your tableView:cellForRowAtIndexPath: method, you can just set the image like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// ...
NSURL* url = GET_IMAGE_URL;
[cell.imageView setImageWithURL:url];
// ...
}
How it works?
The library takes care of looking first in its cache, where it will find the previously downloaded image, because it uses the image urls as keys for the cache, so that the image appears inmediatly.
Issue
You are downloading data and loading it to the tableView async, that means your data process is executing on background, but the tableView is still active and it's shown.
My Solution
I handle this situation in two ways
You first load all the data in a NSArray or NSDictionary (whatever object you need) before presenting your TableViewController, and pass it as an argument to the TableViewController. In other words, you need to load data before presenting the tableViewController.
You need to create an animation view, or a loading view while you are performing the download of the data you tell the user that data is loading so wait until the process is done. And when the process is done, simply reload the tableView and pass the data to the TableViewCells.
Example
1) This code is used when you send request to server to get data (for example from JSON)
LoadingView *spinn = [LoadingView loadSpinnerIntoView:self.view];
dispatch_queue_t downloadQueue = dispatch_queue_create("LoadingView", NULL);
dispatch_async(downloadQueue, ^{
// do our long running process here
[NSThread sleepForTimeInterval:0.1];
// do any UI stuff on the main UI thread
dispatch_async(dispatch_get_main_queue(), ^{
//Start downloading your data, and pass data you fetched to objects of NSDictionary
//Your methods
//After downloading data is done, reload data on tableView
[tableView reloadData];
//Remove loading view, and your tableViewController is all set
[spinn removeSpinner];
});});
2) After fetching data to NSDictionary or NSArray from the request you made, you have also links to download images.
For downloading image async without blocking mainThread you can refer to #sonxurxo question, or you can use this one which also uses SDWebImage
You just need to #import <SDWebImage/UIImageView+WebCache.h> to your project, and you can define also the placeholder when image is being downloaded with just this code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}
// Here we use the new provided setImageWithURL: method to load the web image
[cell.imageView setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
cell.textLabel.text = #"My Text";
return cell;
}
I have multiple collection views on screen at one time that scroll horizontally. They are all filled with images. All of these images are being loaded in the background through the Parse api. I am running Instrument's allocations and the anonymous VM: ImageIO_JPEG_DATA category is taking up a majority of the memory being used. All memory in the app equals to about 40 and then this category is over 55, which puts the total right around 100. That category never goes down at all and just stays consistent. What can I do to free up this memory from the images in my collection views?
Here is the code for my collection view:
.m for my collection view controller
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectionViewCell" forIndexPath:indexPath];
if (cell) {
[cell.loadingImageIndicator setHidden:NO];
[cell.loadingImageIndicator startAnimating];
id photo = [self.collectionViewPhotos objectAtIndex:indexPath.item];
if ([photo isKindOfClass:[PFObject class]]) {
PFObject *photoObject = (PFObject *)photo;
PFFile *imageFile = [photoObject objectForKey:kPhotoPictureKey];
[cell.cellImage setFile:imageFile];
[cell.cellImage loadInBackground:^(UIImage *image, NSError *error) {
[cell.cellImage setContentMode:UIViewContentModeScaleAspectFill];
[cell.loadingImageIndicator stopAnimating];
[cell.loadingImageIndicator setHidden:YES];
}];
} else if ([photo isKindOfClass:[UIImage class]]) {
[cell.cellImage setImage:photo];
[cell.cellImage setContentMode:UIViewContentModeScaleAspectFill];
}
}
return cell;
}
.m for CollectionViewCell
- (void)prepareForReuse
{
self.cellImage.image = nil;
}
- (void)dealloc
{
self.cellImage = nil;
}
Edit: Photo of instruments
I had the same issue in a photo gallery-type app, and ran into the same problem with allocations in the so-called ImageIO_JPEG_DATA category accumulating and remaining "live" forever, causing my app to run out of memory. Oddly, this happenned only on the iPad I was testing on and NOT on the ios simulator (which showed no memory problems).
Brian's suggestion (below) worked for me. My app was originally using an array, each element of which contained - amongst other things - a UIImage. The images were used in various UIScrollViewControllers.
When I needed to load an image, if I used
[UIImage imageWithContentsOfFile:path]
rather than a direct reference to the UIImage in my array, the memory problem (caused by some inexplicable caching that ImageIO_Malloc was doing) was gone and the ImageIO_JPEG_DATA allocations stopped piling up and got released.
I would never have come up with this solution in a gazillion years on my own, so thanks to Brian.