I am building an Xcode application that features a vertically scrolling UICollectionView of Views with large ImageViews in them, similar to Instagram. This Collection View originally loaded very small images into the ImageViews in each UICollectionViewCell, making the app load fast and making the loading of individual cells instantaneous.
I recently made a change to the app that makes the Collection View Cells load much larger images into their Image View, which causes 2 things in some cells:
1) A wait time where the ImageView is blank until the image is loaded
2) Temporary repetition of old images in cells that have been reused from the queue while the new image is loading
Below is the code that I use to initialize my cells:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell;
NSString *CellIdentifier;
UIImageView *thumbnailImage;
CellIdentifier = #"FollowersFeed";
cell = [collectionView
dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
thumbnailImage=(UIImageView *)[cell viewWithTag:1];
dict=[follwerFeed objectAtIndex:indexPath.row];
NSString *nurl=[dict valueForKey:#"video"];
[thumbnailImage setImageWithURL:[NSURL URLWithString:nurl]];
//........
return cell;
}
In order to fix the functionality of this app with these new larger pictures, I need to solve the loading problem by
1) Showing an Activity Indicator spinning while the Image View in a Collection View loads.
2) Remove the image link from the previous Image View before the cell is reused in the queue.
I believe that if I can figure out how to do both of these, then the app's scrolling will work just as seamlessly now as it was with the smaller images
You can try https://github.com/rs/SDWebImage - Asynchronous image downloader with cache support as a UIImageView category
Easy to use:
#import <SDWebImage/UIImageView+WebCache.h>
...
[imageView sd_setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
You can try NSURLRequest
NSURLRequest *request = [NSURLRequest requestWithURL:imageUrl];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response,
NSData * data,
NSError * error) {
if (!error){
cell.thumbnailImage.image = [UIImage imageWithData:data];
}
}];
Related
I have a TableView with custom cells, and for some reason there will be a Table View Cell added that doesn't calculate its height (like these two examples):
The Cell finally calculates it when I scroll down past the blank cell and then back up to where it was before, and I can see the Images "flashing" and placing themselves in the correct spot in the Table View. So I assume it has something to do with the image's in my Table View not loading immediately or something?
Seems to happen in my testing mostly when there are two Table View Cells next to each other in the Table View that have Images in them.
I've tried a bunch of different things, have you ever heard of this? Any idea how to fix? Thanks!
I'm using AutoLayout and Storyboard.
UPDATE
Per Request, adding cellForRowAtIndexPath code:
__weak TableViewCellTwo *wcell = api2Cell;
[wcell.imageViewPic
sd_setImageWithURL:[NSURL URLWithString:imageURL]
placeholderImage:nil
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
// Put Log
}
];
UPDATE
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return tableView.rowHeight; // return the default height
}
I'm building a UICollectionView and my custom cells will contain two labels and one image.
Each image is downloaded asynchronously so I don't know it's size until the download it's complete.
Once is downloaded, I want to adapt each cell to re-layout it's content and frame to fit in height the image just downloaded.
As UICollectionViewLayout, I'm using CHTCollectionViewWaterfallLayout
To download the image asynchronously I'm using SDWebImage, like this:
[cell.imageView setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType)
{... some completion code here ...}];
QUESTION:
What is the right approach to resize each UICollectionViewCell right after the image is downloaded?
You should just be able to invalidate your collection view layout in an animation block when the image comes back. Things might get a little complicated if more than one image finishes completion at once, but this should work:
[cell.imageView setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType)^{
[UIView animateWithDuration:0.3f animations:^{
[self.collectionView.collectionViewLayout invalidateLayout];
}];
}];
Then just return a different size in the appropriate delegate method.
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return /* a different size if the image is done downloading yet */;
}
To resize collection view cells, you could reload the collection view and return the size of you collection view cells dynamically in the method:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
return [cell size];
}
In cases when the cell's size is dynamic, get the cell from the index path and return its size based on the size of the image. I usually create a method for the cell to return a dynamic size, like in the example above. You can use UIImage's size property to help return the size based on the image.
You could try to reloadItemsAtIndexPaths: for the cells that finished loading.
There is my question:
i have custom class that parse through an XML and get string i need to use as URL for my strings, now i modified my code as follow:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
UILabel *labelText = (UILabel *)[cell viewWithTag:1000];
labelText.text = [[self.listOfPlaceDetails objectAtIndex:indexPath.row] objectForKey:#"name"];
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
NSURL *imageURL = [NSURL URLWithString:[[self.listOfPlaceDetails objectAtIndex:indexPath.row]objectForKey:#"imageCell"]];
NSData *image = [[NSData alloc] initWithContentsOfURL:imageURL];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:image];
});
});
return cell;
}
This is pretty straightforward, but, i got unpredictable errors! During scrolling table, images start to chaotically change, sometimes it show 3 or more images and final image is correct one, sometimes final (correct) image does not appear at all. Also, when table is first shown, its actually blank, so i need to scroll it bottom, and then up again to see my images!
In attempt to fix that, i add following code, to determine is my image link correct for that indexPath:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
NSURL *imageURL = [NSURL URLWithString:[[self.listOfPlaceDetails objectAtIndex:indexPath.row]objectForKey:#"imageCell"]];
NSLog(#"%#", imageURL);
}
And when i tap to any cell, it does show me proper link in console log, but image on cell is one of the image shown before (invalid), and it is not the image for that link. How to fix that weird errors?
Any advice would be appreciated, thank you.
When you dequeue a cell object, most of the time you'll get a reused cell i.e. a cell that has been configured by tableView:cellForRowAtIndexPath: once or more times before.
To visualise what's happening in your case, consider one likely sequence of events for a single cell as you perform a long, quick scroll:
The cell is created and an image load is spun off in the background
The cell is scrolled off screen, so added to the table view's cache, ready for dequeuing. The image loading is not canceled at this point
The cell is dequeued and an image load is spun off in the background
Steps 2 and 3 are repeated a few times
The cell is visible, but the several image loading tasks are now completing and each is updating the cell's imageView with the loaded image. This will indeed look like the images are chaotically changing as each loading operation finishes.
(What's more, with a concurrent queue, there's no guarantee that the image loads will complete in the order that they're started - you may not end up with the correct final image!)
So what do we do about it? Now that we understand the problem, there are lots of different solutions. A very simple solution (that I don't really recommend) is to check that the cell's label text matches the value for that indexPath, when you come to set the image:
if ([labelText.text isEqualToString:[[self.listOfPlaceDetails objectAtIndex:indexPath.row] objectForKey:#"name"]]) {
cell.imageView.image = [UIImage imageWithData:image];
}
Obviously, this assumes that all the place details have unique names.
A better solution might be to create an object that handles the image download, and is something that you can register/unregister cells against to handle download completion. This object could enforce the condition that a cell cannot be waiting for more than one image load. As #Leena pointed out, caching is a good idea and this object could be responsible for that too.
As for the blank images, calling [cell setNeedsLayout] after setting the image should sort that out.
Default property "imageView" will not be added to the cell until its (imageView's) property "image" is nil (you can check cell.imageView.superview will be nil too).
That's why your tableView is blank when it is loaded and all images for cells are also loaded.
So when you scroll it down (or up) cells will be reloaded, their "imageView" will have image data. That is the reason why they are on cell and you can see them.
The other problem is that your images are flashing all the time. It happens because your cells are dequeued.
So, when the image for the first cell is downloaded and setted it will not be shown until you relayout cell's subviews (for example by calling [cell setNeedsLayout];-).
And when you scroll table down, your first cell (with an image now) will be dequeued from "tableView" and will become last cell, and then your first cell will be shown with image that actually belongs to the first row.
At the same time you start downloading image for this (last) row and after it was downloaded you will set it. And this is the monent when flashing happens.
I'm setting the image of my custom cell like this.
__weak CustomCell *weakcell = cell1;
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:postPictureLink]];
//it uses AFNetwork's UIImageView+AFNetworking extension
[cell1.postImageView setImageWithURLRequest:request placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
weakcell.postImageView.image = image;
//this method call changes cell height according to image size
[weakcell layoutSubviews];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
}];
But then how do I implement
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
method so that it can give different heights according to size of my cell? Because of images size can vary different cells have different heights.
This is very hard to do. One option would be to somehow get the size from the API you are getting your image from.
A second option would be to only show your cell after the image is downloaded. You can then get your height from the image itself.
Lastly you could reload your rows after the image is downloaded. This might cause a few weird animation issues as your images download.
I have done the similar what you want to do, for this the APIs written will return me the size of the image with the URL, so I know how much size it going to take when dowloaded from server so I give the height according to that.If you see flickr or any other APIs for images you can see there is an option to get the size of the each different resolution image.
Keep in mind that your table view cells can (or should) be reused with the dequeueReusableCellWithIdentifier: method, so you don't want to store the images on the table cells.
A better solution would be to maintain an array of images that you can query in heightForRowAtIndexPath, as well as using it when you configure the actual cells.
I want to load an image onto a table view cell, i.e., a custom cell with an image view. And, I use SDWebImage. I am loading an image onto the cell without using setImageWithURL. This is the code inside cellForRowAtIndexPath.
[_imgManager downloadWithURL:urlArray[indexPath.row]
completed:^(UIImage *image, NSError *error//yada, yada) {
if(image)
{
NSLog(#"Image received");
//cell.pictureView.image = image; // doesn't work, so I did
dispatch_async(dispatch_get_main_queue(), ^{
UITableViewCell *tCell = [self.tableName cellForRowAtIndexPath:indexPath];
if(tCell)
tCell.imageView.image = image;
}); // doesn't work either
}
}];
So as I have mentioned in the comments, it doesn't work. What am I doing wrong? Or maybe my conception of this is wrong? The images load only after I scroll (that activates cellForRowAtIndexPath for other cells). And they keep refreshing on each appearance. It doesn't work exactly as expected.